Agent 架构文档 · Markdown
OpenClaw Agent 架构全景讲解
以 Agent 的生命脉络 为主线,讲清楚 OpenClaw 这套"长在你电脑上、活在你 IM 里"的 个人 AI 助手是怎么被定义、被唤起、怎么思考、怎么记忆、怎么开口、怎么协作的。 所有概念(Gateway / Agent / Skills / Souls / Hooks / Nodes / Canvas / MCP) 都将从"它支撑了 Agent 的哪
以 Agent 的生命脉络 为主线,讲清楚 OpenClaw 这套"长在你电脑上、活在你 IM 里"的 个人 AI 助手是怎么被定义、被唤起、怎么思考、怎么记忆、怎么开口、怎么协作的。 所有概念(Gateway / Agent / Skills / Souls / Hooks / Nodes / Canvas / MCP) 都将从"它支撑了 Agent 的哪个能力"这个角度切入,而不是按模块清单堆叠。
适合:第一次读 OpenClaw 源码 / 想把它当 SDK 拿来改 / 想拿它和别家 agent 框架对位时心里有数。
文档对齐基准:本版基于
openclaw/openclaw@main(package.jsonversion2026.5.20, 推荐 Node 24,最低 Node 22.19+)。2026.05.21 复核:拿 docs.openclaw.ai 与仓库 README 重新对位,标注差异点。 相比上一版(2026.05.11)的主要变化集中在 §0.6(新增子系统总览)、§1.3(workspace 文件多了USER.md/HEARTBEAT.md/BOOTSTRAP.md/BOOT.md/DREAMS.md)、§4.1(bundled tools 现在带媒体生成/ Web 抓取/计划工具)、§5.2(bundled hook 实际有 5 个,session-memory 触发时机改为command:new/reset且文件名走 LLM slug)、§6.1(通道适配 21 个、Canvas 改为同端口能力路径而非独立 18793 端口)。
#目录
- 写在前面:项目身份与核心矛盾 0.5. 一张图看懂:OpenClaw = Model + Harness 0.6. 2026.05 新增子系统总览
- Agent 是什么 —— Workspace + IDENTITY + SOUL/AGENTS/TOOLS
- Agent 怎么活起来 —— Gateway 收信、路由到 Agent、起 Run
- Agent 怎么思考 —— 单 Run 串行的 ReAct Loop + Sub-Agent 委派
- Agent 怎么行动 —— Skills / Bundled Tools / MCP / Plugins 四类工具
- Agent 怎么记忆 —— MEMORY.md + 日记 + Active Memory 子代理
- Agent 怎么开口 —— Channels、Nodes、Canvas、WebSocket 事件总线
- Agent 的外脑 —— 模型无关层与 OpenRouter / iblai-router 接入
- 关键设计权衡(架构师视角)
- 附录:组件地图、关键文件、源码索引
#零、写在前面:项目身份与核心矛盾
#项目消歧
"OpenClaw" 这个名字字面有歧义(同名旧项目是一个 Claw 系列 2D 平台游戏的开源 fork, 跟 AI 一点关系没有)。但我们这次说的 OpenClaw 是一个 2025 年下半年起步、2026 年初爆红 的开源个人 AI agent:
- 官方仓库:https://github.com/openclaw/openclaw
- 官方站:https://openclaw.ai/、文档站:https://docs.openclaw.ai/
- 历史名字:Clawdbot → Moltbot → OpenClaw(因 Anthropic 商标投诉而连续改名)
- 维护者:Peter Steinberger(独立开发者,后聚集起 ClawHub / NemoClaw / OpenClaw-RL 等周边)
- 协议:MIT
- 主要语言:TypeScript(runtime)+ macOS/iOS/Android 原生 companion
多 agent 协作。它解决的是一件极具个人色彩的需求:
"给我一个自己的 24 小时在线助手,能从 WhatsApp / iMessage / Telegram 任意聊天软件 里被我喊起来、能调本地工具能上网能写文件能用我的日历邮箱、最好还能在 macOS / iOS 上用语音说话给我听。"
#三条核心矛盾
| 矛盾 | OpenClaw 的回答 |
|---|---|
| 想随时被找到(多 IM 接入),但 LLM 又是单线程的 | Gateway 守着所有 channel,按 binding 路由到 agent;agent 内部 Run 串行(session lane 锁) |
| 要 24×7 跑在用户机器上,但用户机器不是服务器 | 全套以 macOS 菜单栏 + iOS/Android 节点为 first-class,CLI 一条 openclaw onboard 起家 |
| 生态要开放(5400+ skills),但本地敏感数据要安全 | Skill 走文件 + frontmatter,需要的能力声明在 metadata.openclaw.requires;DM pairing、hook token 与 gateway token 隔离、可选 Docker sandbox |
后面每一节都会回到这三条矛盾,看具体的设计是怎么回应的。
#跟 Claude Code / Cursor / Cline / Aider / Cody 的差异(一句话)
OpenClaw 不是一个 IDE 协作的 coding agent;它把"agent" 当成一个长跑的个人助手进程, 聊天软件是它的输入设备,本地文件 / 应用 / Canvas 是它的输出设备。Coding 只是它能装进 来的众多 Skill 之一,而不是核心场景。这个定位差异会贯穿后面所有章节。
#零·五、一张图看懂:OpenClaw = Model + Harness
业界视角:Anthropic / Cursor / Codex / Aider / Cline 用的是同一批底层模型, 实际表现差距却很大。差的不是模型,是"模型之上的外壳(Harness)"——指令、工具、 基础设施、可观测性这四层共同决定了 agent 的真实能力上限。
套到 OpenClaw 上有趣的是:它的 Harness 不是为了赢 SWE-bench,而是为了在你电脑上 24 小时活着——所以指令层做了 SOUL/SKILL 双轨可发布、基础设施层把"手机当感官"做成 Nodes、记忆层用纯 markdown 文件代替 vector DB。下面按"4 层 + 8 支柱"反向索引。
#0.5.1 四层 Harness 结构
┌──────────────────────────────────────────────────────────────────────┐
│ Agent = Model (LLM) + Harness (OpenClaw 四层外壳) │
│ └ 透传 OpenRouter/Ollama └ 单进程 Gateway 吃下所有 IO │
└──────────────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────────┐
│ ① 指令层 │ IDENTITY.md(名字/emoji/avatar) → §1.1 │
│ │ SOUL.md(可发布的人格,ClawHub 流通) → §1.1 │
│ │ AGENTS.md / TOOLS.md(协作规则 + 工具说明) → §1.1 │
│ │ openclaw.json bindings 路由表 → §1.2 │
├──────────────────────────────────────────────────────────────────────┤
│ ② 能力层 │ Bundled Tools (~25 个,TS in-proc,含媒体生成) → §4.1 │
│ │ Skills (5400+ 社区,仓库内置 53) → §4.1 │
│ │ MCP servers(stdio 已实现;SSE/HTTP via plugin) → §4.1 │
│ │ Plugins(registerTool 主路径 + bundle entry) → §4.1 │
│ │ 6 层加载优先级 + session 启动打快照 → §4.1 │
├──────────────────────────────────────────────────────────────────────┤
│ ③ 基础设施│ Gateway daemon (单进程, 127.0.0.1:18789) → §2.3 │
│ │ Canvas(与 Gateway 同端口,/__openclaw__/canvas 路径)→ §6.4 │
│ │ Nodes(iOS/Android/macOS WS 客户端) → §6.3 │
│ │ 21 个 Channel adapters(extensions/<name>) → §6.2 │
│ │ session lane(process/lanes.ts CommandLane) → §3.2 │
│ │ Docker sandbox(sub-agent 默认隔离 + profile)→ §4.3 │
│ │ Cron(agent 自己的闹钟,isolated agent 模式) → §6.6 │
│ │ Talk / RealtimeTranscription(实时语音通道) → §0.6 │
│ │ Proxy-Capture(调试代理,自带 CA) → §0.6 │
├──────────────────────────────────────────────────────────────────────┤
│ ④ 可观测 │ WebSocket 事件总线(统一帧格式 + JSON Schema) → §6.5 │
│ │ agent:lifecycle / tool:start-end / cron:fired │
│ │ Run-level tokens 统计(不是计费系统) → §7.2 │
│ │ ⚠ trace / cost 看板 / 失败率聚合 — 不存在 │
└──────────────────────────────────────────────────────────────────────┘
#0.5.2 八支柱对照表
| # | Harness 支柱 | 一句话定义 | OpenClaw 的实现 | 对应章节 |
|---|---|---|---|---|
| 01 | FS + Git | state 必须活在 context 之外 | workspace 目录就是 agent 的私人办公室:IDENTITY/SOUL/AGENTS/TOOLS.md + MEMORY.md + memory/YYYY-MM-DD.md + sessions/*.jsonl 全是文件,可 cat、可 git、可分享 |
§1.3 / §5.1 |
| 02 | Bash 通用工具 | "随用随装",避免 token 爆炸 | Skills 6 层加载优先级(workspace → .agents → ~/.agents → ~/.openclaw → bundled → extraDirs)+ frontmatter requires.env/bins/os gating,加载时静默跳过 + 一条诊断 |
§4.1 |
| 03 | 沙箱 + 子工具 | 行动可隔离 + 可自检 | Docker sandbox 默认 sub-agent 用(只放 bash/read/write/edit/sessions,禁 browser/canvas/nodes/cron);hook token ≠ gateway token 强制隔离 | §4.3 / §8.6 |
| 04 | 记忆 + 搜索 | 跨 turn / 跨 session 拿历史 | 三层结构:MEMORY.md(永久事实)+ memory/YYYY-MM-DD.md(日记按天分桩)+ session.jsonl;全是 markdown 文件,不进任何数据库 |
§5.1 / §5.3 |
| 05 | 对抗 Context Rot | 长对话也别糊 | command:new / command:reset 触发 session-memory 写记忆 → session:compact:before/after 包住模型驱动的 compact;session.idle / session.maxAge 参数化兜底(无固定 daily 时间) |
§5.2 / §5.4 |
| 06 | 长程执行 | 多 phase 任务不跑偏 | session lane 锁让单 session 内 Run 严格串行(append-only transcript 不能并发写);要并发只能显式 spawn sub-agent(独立 session + uuid + 默认 isolated 上下文) | §3.2 / §3.3 |
| 07 | Hooks 强制层 | 失败转成永久规则 | hook 链就是它的软件总线:message:received/transcribed/preprocessed / PreToolUse / PostToolUse / session:compact:before/after / command:new;每个 hook = HOOK.md + handler.ts 可拦截/改写/丢弃 |
§2.4 / §3.1 |
| 08 | 规划 + 工具选择 | 别什么都试,先想再调 | Active Memory 子代理:在主 LLM 调用前阻塞跑一个隐藏 sub-agent(只有 memory_search/get 工具),把记忆 summary 强制注入 system 段——不信任主 LLM 会"自己想起来去查记忆" |
§5.3 |
#0.5.3 当前架构的短板(按 Harness 视角看出来的)
OpenClaw 的 Harness 是专门为"个人 24×7 助手"这个非主流场景设计的,所以它的短板都不是 "设计缺陷",而是"定位选择的副作用"——但用 Harness 通用视角看,这些副作用真实存在:
- ❌ 单用户单设备硬绑定:openclaw.json 在每台 host 上一份、Gateway 监听
127.0.0.1、 session lane 锁是本机文件锁——这套架构不支持"一个 agent 在两台机器上接力"。想多设备 访问只能靠 Tailnet 把 18789 暴露出去,或显式 pairing;workspace 在两台机器间不会 自动同步(要自己上 git/iCloud Drive/Dropbox)。 - ❌ IM 协议适配脆弱:22+ 个 channel adapter 里至少一半依赖非官方协议——WhatsApp 走 Baileys(多账号实例反向工程),iMessage 走 BlueBubbles 或 macOS 私有桥。这些协议 随平台升级容易断,adapter 维护成本高,断了 agent 就"听不见"——这是 OpenClaw 用户群里 反复反馈的痛点。
- ❌ 多设备/多 Gateway 同步靠手动:单进程 daemon "exactly one Gateway per host" 是它的设计宣言,但代价是没有跨 host 的 agent 协作协议;如果你想让"家里 Mac 的 agent 和公司 Mac 的 agent 共享一份 MEMORY.md",得自己用 git / Syncthing 拉同步。Sub-agent 也只是 同进程内的另一个 session,不是跨机器的远程 agent。
- ❌ 没有正式的 cost / cycle observability:每次 Run 会统计 tokens 写到 transcript, WebSocket 事件总线推 lifecycle / tool / cron 事件——但没有"按 agent / 按 SOUL / 按 SKILL 维度的 cost 聚合看板",没有"哪个 skill 调用失败率最高"的分析视图,没有跨 session 的 trace。Harness 视角下的"可观测层"对 OpenClaw 来说基本是空白——这跟它"个人助手不是 企业 agent"的定位匹配,但任何想拿它做团队/小公司部署的用户都会立刻撞上这堵墙。
值得肯定的是:把可观测层做得这么薄,反而给了社区扩展空间——yoloshii/ClawMem 把
"持久记忆 + 混合 RAG"做成 hook 包,iblai/iblai-openclaw-router 把"多 provider 计费"
做成外置路由。Harness 的可观测性最终可能不是核心仓库的事,而是社区 hook 生态的事。
#零·六、2026.05 新增子系统总览(与"个人助手"定位的扩张)
这一节不是叙事章节,而是给读者一份"目前 src/ 多了哪些之前没听过的目录"的速查—— 它们都是为了支撑 OpenClaw "做一个长期共生伙伴"的产品野心,而不是 coding agent。 后续章节按需引用这些子系统,本节只做一次性扫盲。
| 目录 | 出现的原因(产品意图) | 关键文件 |
|---|---|---|
src/talk/ |
实时语音对话:电话/麦克风做主交互通道,不再只是 IM 文字。TalkProvider / TalkbackRuntime 跨 STT/LLM/TTS 编排 |
provider-types.ts、agent-talkback-runtime.ts |
src/realtime-transcription/ |
给 Talk 用的实时 STT provider 抽象(Deepgram/Azure/Whisper 都能挂) | provider-registry.ts |
src/auto-reply/ |
"我不在/勿扰" 类的自动回复策略,独立于主 agent loop | model-runtime.ts、types.ts |
src/tasks/ |
任务流:把多步任务建模成"detached task" 自己跑(不占主 session 的 lane) | task-flow-registry.ts、detached-task-runtime.ts |
src/commitments/ |
承诺跟踪:从对话里抽取 "我答应你周三给报告" 这种承诺,到点提醒。Agent 不再靠 cron 凭空起 | extraction.ts、runtime.ts、store.ts |
src/context-engine/ |
上下文工程:把"该塞什么进 system prompt"从 ad-hoc 拼字符串升级为 ContextEngineDelegate + registry |
delegate.ts、registry.ts |
src/crestodian/ |
对话审计:dialogue / audit service,给"长期助手要回放、要解释"的需求兜底 | dialogue.ts、audit.ts |
src/trajectory/ |
把整段 Run 导出成 trajectory bundle(JSONL),用于离线分析或喂回 RL | types.ts、export.ts |
src/proxy-capture/ |
一个内置的 HTTPS 代理 + CA,给 agent 调试外部 API 用("我让 agent 访问的 API 到底回了啥") | proxy-server.ts、ca.ts |
src/memory-host-sdk/ & packages/memory-host-sdk/ |
把"记忆"从单 markdown 扩成可被第三方 host 的 engine + dreaming 流程 | engine-storage.ts、dreaming.ts |
src/model-catalog/ |
模型清单 / manifest 规划:定价、能力(reasoning/vision/audio)、配额,独立于 provider 适配 | manifest-planner.ts |
src/provider-runtime/ |
provider 调用的统一 retry / circuit / fallback 包装 | operation-retry.ts |
src/node-host/ |
节点侧"我有权限跑什么"的策略层 + SystemRunInvoker |
exec-policy.ts、invoke-system-run.ts |
src/pairing/ |
设备配对协议(手机←→Gateway)、challenge-response | pairing-store.ts、challenge.ts |
src/secrets/ |
凭证/环境变量管理("哪个 skill 需要哪个 secret 已声明") | plan.ts |
src/status/ |
UI 看到的 "agent 现在在干嘛" 状态文本队列 | status-text.types.ts、status-queue.runtime.ts |
src/plugin-state/ |
插件持久化状态(per-install scope,不依赖文件系统约定) | plugin-state-store.ts |
src/web-fetch/ & src/web-search/ |
web fetch / search 升级成自己的 runtime(提供商可换) | runtime.ts |
src/link-understanding/ |
URL 元数据抽取("用户贴了个链接"自动展开) | detect.ts、apply.ts |
src/media*/ (media, media-generation, media-understanding) |
媒体管理(本地资源管理 + 生成 + 理解的三件套) | media/store.runtime.ts |
src/image-generation/ / music-generation/ / video-generation/ / tts/ |
四个独立的生成 provider registry。配合 bundled tools 里的 image-generate / music-generate / video-generate / tts |
provider-registry.ts |
src/acp/ |
Agent Control Protocol 类型 + persistent bindings(用于 ACP 兼容客户端如 acpx) | types.ts、persistent-bindings.ts |
packages/plugin-package-contract/ |
插件包合约——区别于运行时 plugin-sdk,定义"打包/发布"侧的契约 |
package.json 内 contract 类型 |
怎么用这张表:当后续章节提到 "Active Memory 子代理"、"trajectory 导出"、"语音 Talk Mode" 时, 回这里看一眼对应目录的实际文件位置。OpenClaw 在 2026 Q1 之后大幅扩张了"非 IM 文字"输入输出 (语音/媒体生成/承诺/任务流),所以 src/ 目录长得比上一版讲解多了快一倍——但核心 agent loop 和 Gateway 的设计骨架没变。
#一、Agent 是什么 —— Workspace + IDENTITY + SOUL / AGENTS.md / TOOLS.md
关键认知:在 OpenClaw 里,"一个 Agent" ≠ 一个进程,而是一份 workspace 目录 + 一份 IDENTITY/SOUL 文档 + 一份 binding 路由表。 同一个 Gateway 进程下挂多个 agent, 每个 agent 有自己的 workspace、自己的会话历史、自己的人格。
#1.1 Agent 的"身份证":四类描述文件
OpenClaw 的 agent 配置不是一行 YAML,而是一组分工明确的 markdown 文件,注入到模型 system prompt 的不同位置:
| 文件 | 角色 | 类比 |
|---|---|---|
SOUL.md |
人格/价值观/语气/拒绝什么——agent 的"灵魂" | 演员的角色设定本 |
AGENTS.md |
多 agent 协作的根规则、子目录 scope(兼容符号链接 CLAUDE.md) |
公司员工手册 |
TOOLS.md |
当前 workspace 里能用的工具白名单/用法 | 工具间的钥匙板 |
IDENTITY.md |
名字、emoji、avatar、theme(外观) | 工牌 |
USER.md(新增) |
用户画像/喜好的稳定快照(被注入 system 段,给 agent 一个"主人是谁"的最小信息) | 主人档案 |
HEARTBEAT.md(可选) |
周期心跳要做的事——配合 infra/heartbeat-runner.ts 周期触发 |
巡店清单 |
BOOTSTRAP.md(可选) |
首次启动一次性仪式(创建文件、问 API key 等) | 入职单 |
BOOT.md(可选) |
每次 Gateway 启动时执行的指令(需启用 boot-md bundled hook) |
开机自检 |
DREAMS.md(可选,非官方) |
dreaming sweep 的累积摘要(配合 memory-host-sdk/dreaming.ts),供人类回看;docs.openclaw.ai/concepts/agent-workspace 当前列表里没列 DREAMS.md,它是 SDK / 社区扩展层把"dreaming 总结"落盘的约定(待核实是否升入主流程) |
梦境日记 |
这些文件在 agent 启动时按顺序读出并按段拼到 system prompt,组合成 agent 的初始人格。
官方 docs.openclaw.ai/concepts/agent-workspace 列出的"workspace 根文件"共 9 个
(IDENTITY / SOUL / AGENTS / TOOLS / USER / MEMORY / HEARTBEAT / BOOT / BOOTSTRAP),
DREAMS.md 不在该列表内,是社区/SDK 层的扩展约定(见上表脚注)。
根 markdown 从最早的 4 个扩到现在的 9 个——背后逻辑是:长跑 agent 比单 session
agent 需要更多"出厂资料",把"用户画像 / 心跳 / 启动仪式"都正交化成独立 markdown,让用户
和社区都能改。各文件之间的拼接顺序仓库目前没有文档化,按 src/agents/harness/
实现合理推断。
其中 SOUL.md 是最有意思的——它被独立设计成一个可发布、可复用、可在 ClawHub 上交易
的最小单元(参见 ClawHub docs/soul-format.md):
# SOUL.md 顶部 frontmatter(来自 clawhub 的 spec)
---
name: "lobster-mom"
description: "An overprotective mom who happens to be a lobster"
version: "1.2.0"
tags: ["personality", "lobster", "caring"]
---
# 下面是大段 markdown,描述这个角色应该怎么说话、怎么拒绝、什么时候转移话题
重要洞察:SOUL 是"可分发的人格",SKILL 是"可分发的能力",两者是 OpenClaw 生态 给社区交付的两个最基础的乐高块。一个 agent ≈ workspace + 1 个 SOUL + N 个 SKILL + M 个 channel binding。
#1.2 Agent 的"户口":openclaw.json
所有 agent 在中央配置文件 ~/.openclaw/openclaw.json 里登记:
{
"agents": {
"list": [
{
"id": "main",
"workspace": "~/.openclaw/workspace/main",
"skills": ["browser", "canvas", "memory-search"],
"model": "openrouter/anthropic/claude-sonnet-4.5"
},
{
"id": "ops",
"workspace": "~/.openclaw/workspace/ops",
"skills": ["nodes", "shell", "kubectl"]
}
],
"defaults": {
"skills": ["bash", "read", "write", "edit"]
}
},
"bindings": [
{ "channel": "telegram", "accountId": "ops_channel", "agentId": "ops" },
{ "channel": "whatsapp", "accountId": "*", "agentId": "main" }
]
}
这套配置由 openclaw agents add / bind / unbind / set-identity 等子命令来管。
理解 openclaw.json 的关键点:
agents.list[].skills是这个 agent 能见到的工具白名单(基于agents.defaults.skills叠加)bindings决定从哪条聊天通道进来的消息会落到哪个 agent——多 agent 路由的全部秘密- 每个 agent workspace 物理上是一个目录,里面有
IDENTITY.md、本地 skills、session 转写、memory 文件,workspace 是隔离单元
#1.3 Workspace = Agent 的"私人办公室"
workspace 目录的典型结构:
~/.openclaw/workspace/main/
├── IDENTITY.md ← 名字/emoji/avatar
├── SOUL.md ← 人格(可选,没有则继承默认)
├── AGENTS.md ← 协作规则(兼容 CLAUDE.md 软链)
├── TOOLS.md ← 工具说明
├── USER.md ← 用户画像(2026.04 新增)
├── MEMORY.md ← 长期记忆(详见第五章)
├── HEARTBEAT.md ← 心跳清单(可选,可被 heartbeat-runner 触发)
├── BOOTSTRAP.md ← 首次启动一次性仪式(可选)
├── BOOT.md ← Gateway 启动时执行(可选,需 boot-md hook)
├── DREAMS.md ← dreaming sweep 摘要(可选)
├── memory/
│ ├── 2026-05-12.md ← 当日日记
│ └── 2026-05-13-1830-foo.md ← 由 session-memory hook 写入,文件名带 LLM 生成的 slug
├── skills/ ← workspace 私有 skills
│ └── my-custom-skill/
│ └── SKILL.md
├── sessions/ ← 会话转写(session.jsonl,含 sub-agent session)
├── hooks/ ← workspace 私有 hooks(覆盖 bundled)
└── .agents/ ← project-scoped agent 配置 / skills / hooks
跟 Claude Code 里 .claude/ 加 CLAUDE.md 那一套是同一个心智模型,但层级换了名字:
| Claude Code | OpenClaw |
|---|---|
~/.claude/ |
~/.openclaw/ |
<project>/.claude/ |
<workspace>/.agents/ |
CLAUDE.md |
AGENTS.md(且支持 CLAUDE.md 作为同名 sibling 软链) |
.claude/agents/ |
agents.list[] 配 + workspace 目录 |
.claude/skills/ |
skills/(多层级,详见第四章) |
.claude/commands/ |
slash commands(详见 4.4) |
settings.json |
openclaw.json |
OpenClaw 比 Claude Code 多的两层是 SOUL(人格独立)和 bindings(IM 路由); 少的一层是 IDE 集成(它压根没那个野心)。
#1.4 一份配置驱动一切:找它就找配置
谁在线: openclaw.json -> agents.list[]
来 X 通道找谁: openclaw.json -> bindings[]
某 agent 能用啥工具: workspace/TOOLS.md + skills 加载链 + agents.list[].skills 白名单
某 agent 是怎样的人: workspace/SOUL.md + IDENTITY.md
某 agent 记得啥: workspace/MEMORY.md + memory/YYYY-MM-DD.md
理解完这一节,"一个 OpenClaw Agent 是什么"就清楚了。下一节看它怎么"活起来"。
#二、Agent 怎么活起来 —— Gateway 收信、路由到 Agent、起 Run
"用户在 Telegram 给 bot 发了一句 '帮我看看刚到的那封邮件',按下回车—— 这条消息要怎么走到 agent 的 system prompt 里?"
#2.1 端到端时序
[1] Telegram 服务器
用户消息 → Telegram Bot API webhook
│
▼
[2] Channel Adapter (extensions/channels/telegram,TS)
│ grammY 客户端把消息封装成统一的 ChannelMessage 帧
│
▼
[3] OpenClaw Gateway (本机 daemon, TS, 默认 127.0.0.1:18789)
│
│ 3.1 入站校验
│ ├─ 验 connect handshake(是 control-plane 还是 node 还是 channel?)
│ ├─ JSON Schema 校验消息帧
│ └─ idempotency 键去重
│
│ 3.2 hook 链:message:received → message:transcribed → message:preprocessed
│ (每个 hook 是 hooks/<name>/HOOK.md + handler.ts,可拦截 / 改写 / 丢弃)
│
│ 3.3 binding 路由
│ lookup({channel: "telegram", accountId: "ops_channel"})
│ → agentId = "ops"
│
│ 3.4 起 Run
│ sessionKey = "agent:ops:tg:<chatId>"
│ acquire session lane lock ← 同一 session 不允许并发 Run
│ assign runId, return {runId, acceptedAt} 给 channel adapter
│
▼
[4] Agent Loop (in-process, pi-agent-core 运行时)
│
│ 4.1 Context Assembly(拼上下文)
│ ├─ 读 SOUL.md / AGENTS.md / TOOLS.md / IDENTITY.md
│ ├─ 读 MEMORY.md + 最近 N 天 memory/YYYY-MM-DD.md
│ ├─ 加载已启用的 skills 的 SKILL.md(按工作区/managed/bundled 优先级合并)
│ ├─ 拉历史 session 转写(如果该 session 之前活过)
│ └─ 跑 Active Memory 子代理(详见第五章),结果作为 hidden context 拼到 system 段
│
│ 4.2 进入 Agent Loop(详见第三章)
│ while not done:
│ response = await model.infer(...) ← 走 model provider,详见第七章
│ emit lifecycle event "model:end"
│ if response.tool_calls:
│ for tc in response.tool_calls:
│ hook chain: PreToolUse / tool:pre ← 可拒绝
│ result = await execute_tool(tc) ← 详见第四章
│ hook chain: PostToolUse
│ yield tool event 到 WS 流
│ else:
│ yield assistant deltas 到 WS 流
│ break
│
│ 4.3 Persistence
│ ├─ 增量写 sessions/<key>.jsonl(带文件锁)
│ ├─ 必要时触发 session:compact:before/after hook(压缩对话)
│ └─ 触发 session-memory bundled hook(把关键事实落到 MEMORY.md)
│
▼
[5] Reply Channel Adapter
│ Gateway 把最终 assistant 文本走回 telegram adapter
│ → bot.sendMessage(chatId, text)
│
▼
[6] 用户的 Telegram 客户端
✓ 看到 bot 回了
#2.2 这条链路的几个关键事实
| 事实 | 含义 |
|---|---|
| Gateway 是单进程 daemon,不是分布式服务 | 整个 OpenClaw 在一台机器上,没有内网调度层 |
| 每个 host 正好一个 Gateway | 文档原话:"Exactly one Gateway controls a single Baileys session per host." |
Gateway 默认监听 127.0.0.1:18789(HTTP + WS 复用同一端口) |
远程访问要走 Tailnet 或显式 pairing;Canvas 现在与 Gateway 共用 18789,路径 /__openclaw__/canvas/ + /__openclaw__/a2ui/(社区代码里能看到细分到 /__openclaw__/cap/<capId>/canvas/... 的 per-capability token 路径,详见 §6.4) |
| Channel adapter 跟 Gateway 用 WebSocket 互通,不是直接函数调用 | 因此 channel 可以扩展(写一个新 adapter,连上 WS 就行);当前 21 个 adapter(extensions/<channel>) |
WS 帧统一 {type: "req"|"res"|"event"} 格式 + JSON Schema 校验 |
这就是它的"内部协议",没有 HTTP REST API。role 取值实际为 node / operator / channel adapter 自报,不是文档化的三选一 |
Run 有 runId、立刻返回 ack,事件流式回推 |
跟 OpenAI streaming 是同一个心智 |
Agent 运行时已迁移到 src/agents/pi-embedded-runner/ + src/agents/harness/ |
不再叫 "pi-agent-core"。harness 暴露 runAttempt / compact / reset 三接口,内部走 ReAct 但带 context-engine-lifecycle 钩子可介入上下文装配 |
#2.3 为什么要有 Gateway 这一层
调度、推送、聚合、鉴权、IM 适配全压在 Gateway 这一个进程里。这是因为它的物理拓扑 就是一台机器:
- 没有"agent 工作器复用"的需求——agent 就在你机器上,启动是 ms 级
- 没有"流推送给多端"的需求——你自己的 Telegram、WhatsApp 跟 macOS app 各订各的
- 不存在多租户——这台机器上就一个用户
但 Gateway 自己干的事并不少:
Gateway 做啥 为什么必须它做
───────────────────────────────── ─────────────────────────────
绑定一堆 channel adapter (20+) 全平台只有一个真长链场景
按 binding 路由消息到 agent IM 通道是用户唯一入口
session lane 锁,Run 串行 避免同 session 并发改 transcript
触发各种 hook 权限/记忆/审计的总抓手
托管 Canvas HTTP 服务 给 agent 一个"画布"输出设备
管理 Cron agent 自己的定时任务调度
管 Nodes (iOS/Android/macOS) 接入 手机充当语音 / 摄像头 / 位置传感器
类比:Gateway 是家里的总配电箱 + 路由器 + 智能音箱中枢的合体——所有外面的信号 进家先到它这里、所有家电对外讲话也走它出去。
#2.4 没有 dispatcher、没有 courier,但有 hook 链
OpenClaw 把它们替换成了hook 链 + WS 事件流:
入站 出站
───────────── ──────────────
message:received ▲ lifecycle:end │
│ │ ↑ │
message:transcribed │ tool:end │
│ │ ↑ │
message:preprocessed│ tool:start ▼
│ │ ↑ 流向 channel adapter
▼ │ assistant:delta 输出到 IM
binding 路由 ────┘ ↑
│ lifecycle:start
▼
Agent Loop
这个 hook 链就是它的"软件总线"。第八章会讨论这个设计的取舍。
#三、Agent 怎么思考 —— 单 Run 串行的 ReAct Loop + Sub-Agent 委派
"Agent 拿到 context 之后是怎么决定下一步做什么?是单纯让 LLM 自己决定,还是有 外层的 plan-execute 框架?" OpenClaw 的回答:朴素的 ReAct,但极度强调"一个 session 同时只跑一个 Run"。
#3.1 单层 ReAct,没有 plan-execute 外壳
OpenClaw 的 agent loop 是经典的 ReAct(Reason + Act):模型自己决定 reason、自己选
工具、自己决定停——官方文档没用 "ReAct" 这个词,但描述出来的形状就是 ReAct,
docs.openclaw.ai/concepts/agent-loop 的原话是:
"Core flow: intake → context assembly → model inference → tool execution → streaming replies → persistence."
"Runs are serialized per session key (session lane) and optionally through a global lane. This prevents tool/session races and keeps session history consistent."
也就是说:所有 reasoning / action selection / stopping 都压在一次模型 inference 里,
框架不在外面套 planner,也没有强制的"先 plan 再 execute"两段式("ReAct-style" 是
作者归纳,不是官方原话)。如果你想要 plan-execute,那就写一个叫 planner 的 SOUL/Skill
让模型自己去模仿这种风格——框架不替你做这个决定。
简化伪代码(综合 docs concepts/agent-loop.md 描述):
// 简化伪代码,对应文档里描述的 lifecycle phases
async function* runAgent(input: ChannelMessage): AsyncGenerator<RunEvent> {
emit({type: "lifecycle", phase: "start"});
// ① 上下文装配
const ctx = await assembleContext({
soul: read("SOUL.md"),
agents: read("AGENTS.md"),
tools: read("TOOLS.md"),
skills: loadSkills(...),
memory: await activeMemoryBlockingSubAgent(input), // 详见 5.3
history: readSessionTranscript(),
});
// ② ReAct 循环
while (true) {
const resp = await model.infer(ctx); // 流式:assistant deltas
if (!resp.tool_calls?.length) {
emit({type: "assistant", delta: resp.content});
break; // 自然结束
}
for (const tc of resp.tool_calls) { // 顺序执行,无并行
const ok = await runHookChain("PreToolUse", tc);
if (!ok) {
ctx.appendToolResult(tc, "denied by hook");
continue;
}
emit({type: "tool", phase: "start", call: tc});
const result = await executeTool(tc); // 详见第四章
emit({type: "tool", phase: "end", call: tc, result});
await runHookChain("PostToolUse", tc, result);
ctx.appendToolResult(tc, result);
}
}
// ③ 持久化
await persistTranscript(ctx); // 文件锁
await maybeCompact(ctx); // session:compact:* hooks
emit({type: "lifecycle", phase: "end"});
}
#3.2 单 Run 串行:session lane 锁
这里是 OpenClaw 跟很多其它框架最不一样的地方:
"Runs are serialized per session key (session lane) and optionally through a global lane. This prevents tool/session races and keeps session history consistent." ——
docs.openclaw.ai/concepts/agent-loop(核实于 2026.05.21)
也就是说:同一个 session(例如 agent:main:tg:42)一次只能跑一个 Run。如果
用户在 agent 还没回完上一句的时候连发三条新消息,后两条会排队,不会并行起 Run。
锁是进程感知 + 文件锁实现的,因此即使 Gateway 进程重启,也能恢复 lane 状态。
实现位置(2026.05 重组后):
- 通用 lane 抽象:
src/process/lanes.ts(CommandLane) - agent embedded runner 里的 lane 解析:
src/agents/pi-embedded-runner/lanes.ts(resolveEmbeddedSessionLane()) - 心跳/活体检测:
src/infra/heartbeat-runner.ts(早期版本散落在src/sessions/下,新版本把锁的语义提到了 process 层,session 层只负责 transcript IO)
为什么要这么严?因为 transcript 是 append-only 的 session.jsonl,并发写就乱套了。
而 transcript 又是 agent 记忆的根基。牺牲并发换一致性,这是个明确的设计选择。
副作用:你不能让 agent 在等浏览器加载的时候同时去回另一句问候——除非你显式 spawn 一个 sub-agent。
#3.3 Sub-Agent:唯一的"并发逃生口"
OpenClaw 不在主 loop 里搞并发,但提供了 sub-agent 机制让你显式 fork 出去跑:
父 session: 子 session:
agent:main:tg:42 agent:main:subagent:<uuid>
│
│ /subagents spawn ops "去看一下生产 pod 的状态"
│ 或者 model 调用 sessions_spawn() 工具
▼
[ session lane 父锁释放 ]
│
│ (并行)
▼
父 agent 继续干别的事 子 agent 在新 session 跑独立 Run
│
│ 完成
▼
announce step:
把 "summary + 状态 + token 统计 + transcript path"
作为一条 user 消息回写到父 session 的 channel
关键设计点:
- isolation 是默认:子 agent 默认
context: "isolated",干净的 transcript, 跟父 session 没有共享历史;想继承上下文就用context: "fork"。 - session 命名带 uuid:
agent:<id>:subagent:<uuid>;nested 时再追加一段。 - announce 回写:子完成后会自动在父的 channel 发一条消息("任务完成 / 失败 / 超时"),
可以通过模型回
NO_REPLY来抑制这个回复。 - 自动归档:sub-agent session 默认 60 分钟后归档(文件改名为
*.deleted.<ts>)。 - slash 命令族:
/subagents list / kill / log / info / send / steer / spawn。
跟 Claude Code 的差异:Claude Code 的 subagents 是
.claude/agents/*.md的 配置式——每个 subagent 是一个 frontmatter 文件,主 agent 通过 Task 工具触发。 OpenClaw 的 sub-agent 是运行时 spawn 出来的 session,没有"subagent 定义文件" 这一层;所有 agent 都是平级 agent,sub-agent 只是"我现在临时多起一个 Run"。
#3.4 单 Run 之外没有"plan/聪明工具调度"
为了避免误解,明确列一下 OpenClaw 没做 的事:
- 没有自动并行 tool_call(即使 LLM 一次返回多个 tool_calls,框架顺序执行)
- 没有自动 plan-execute 两段式
- 没有强制 max_steps 上限(依赖 agent 超时和 LLM idle 超时兜底)
- 没有内置的"血缘追踪 / Voucher Graph"
- 没有内置的"prompt cache 压缩点对齐"机制(取决于 model provider)
它的设计哲学很清楚:核心 loop 保持极简,复杂度推到 hook 和 skill 里去。
#四、Agent 怎么行动 —— Skills / Bundled Tools / MCP / Plugins 四类工具
"工具系统是 agent 框架最容易长得复杂的地方。OpenClaw 选择了把工具拆成 四种存在形态——但接口统一。"
#4.1 四类工具
┌─────────────────────┐
│ 模型看到的 tools 数组 │
│ (OpenAI tools 协议) │
└──────────┬──────────┘
│
┌──────────────┬───────────┴───────────┬──────────────┐
▼ ▼ ▼ ▼
┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Bundled │ │ Skills │ │ MCP Servers │ │ Plugin │
│ Tools │ │ (markdown) │ │ (外部进程) │ │ Tools │
│ │ │ │ │ │ │ (in-proc) │
│ TS in-proc, │ │ frontmatter │ │ stdio/SSE/ │ │ registerTool│
│ 永驻 │ │ + body 触发 │ │ HTTP │ │ + Service + │
│ ~40 个 │ │ ~5400 个 │ │ N 个 │ │ Command │
└─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘
四类工具到 LLM 看起来是同一类:都是 OpenAI tools schema 里的一项。但加载和执行路径 完全不同:
#Bundled Tools("内置工具")
OpenClaw 出厂自带 ~25 个 first-class 工具,TS 实现,源码集中在 src/agents/tools/,跑在
Gateway 进程内。当前清单(按职责分组):
| 工具集 | 实际工具名 | 干嘛 |
|---|---|---|
| 文件/shell | (由 codex/pi harness 提供 exec 等基础工具,见 src/agents/harness/codex-app-server-extensions.ts) |
shell、读写编辑 |
| 会话/子代理 | sessions_list / sessions_history / sessions_send / sessions_spawn / sessions_yield / subagents / session_status |
spawn/查/收发 sub-agent |
| 网络 | web_fetch / web_search |
抓网页、搜结果(provider 可换:Brave/DDG/Exa/Firecrawl/...) |
| 媒体生成 | image_generate / music_generate / video_generate / tts |
调对应 provider registry(fal/comfy/ElevenLabs/...) |
| 媒体处理 | image / pdf |
看图、解析 PDF |
| 节点/外设 | nodes |
调度 iOS/Android/macOS 节点(语音、摄像头、位置) |
| 计划/状态 | update_plan / session_status / heartbeat_response |
显式更新当前计划、回 heartbeat |
| 编排 | cron / gateway / agents_list / message |
闹钟、网关查询、列出 agent、跨 session 发消息 |
| 记忆 | memory_search / memory_get / memory_recall(lancedb 路径) |
翻 markdown 记忆或向量记忆 |
每个 agent 在 openclaw.json 的 agents.list[].skills / tools 里指定能见到哪些。
工具白名单走两阶段:tools.profile 选 preset,然后 tools.alsoAllow / deny 微调。
sub-agent 默认禁用的是 4 个 session 管理工具:sessions_list / sessions_history /
sessions_send / sessions_spawn——避免子代理翻看/打扰兄弟会话、或自己再 spawn 一层
(参见 docs.openclaw.ai/tools/subagents 的 "sub-agent restriction layer")。
当 maxSpawnDepth >= 2 时,第一层 orchestrator sub-agent 会拿回这 4 个再加 subagents,
以便管理它的下层。gateway / cron / browser / canvas / nodes / web_* 不在硬禁名单里,
是否能用由 tools.profile + Docker sandbox 配置决定(生产里通常会进一步收紧)。
#Skills("markdown 触发的能力")
OpenClaw 生态里最大的能力载体就是 SKILL,5400+ 个社区 skill 的来源。 一个 skill 是一个目录:
skills/jira/
├── SKILL.md ← 必需,frontmatter + 步骤描述
├── jira-search.ts ← 可选脚本/资源
└── ...
SKILL.md 的关键 frontmatter(来自 clawhub docs/skill-format.md):
---
name: "jira"
description: "Search, create, update, and comment on Jira issues."
version: "1.4.0"
metadata:
openclaw:
requires:
env: ["JIRA_HOST", "JIRA_EMAIL"]
bins: ["jq"]
anyBins: []
config: []
os: ["darwin", "linux"]
primaryEnv: "JIRA_API_TOKEN"
install:
brew: ["jq"]
always: false
---
# Jira
When the user asks to find a Jira ticket, follow these steps:
1. Use `bash` to call `curl ... /rest/api/3/search`
2. ...
加载策略——6 层优先级(from docs/tools/skills.md):
1. <workspace>/skills ← 工作区私有,最高
2. <workspace>/.agents/skills ← project-scoped agent skills
3. ~/.agents/skills ← personal agent skills
4. ~/.openclaw/skills ← managed/local
5. <install>/skills ← bundled
6. skills.load.extraDirs ← 兜底扩展目录
同名时高优先级覆盖低优先级。Session 启动时打快照——session 跑起来后再改 SKILL.md 默认不影响这一轮(除非开了 watcher)。
skill 加载时会被 gating 过滤掉:
requires.env/bins/config/os任一不满足 → 这次 session 不上 skill- agent allowlist 不在白名单 → 不上
disable-model-invocation: true→ 模型看不到,但用户可以通过 slash command 触发
#MCP Servers
OpenClaw 作为 MCP host 消费第三方 MCP server 的工具;同时也作为 MCP server 对外暴露 OpenClaw 自己的工具。
- 当 host 消费时:通过
mcporter这个 skill 把任意 MCP server 接入(stdio/SSE/HTTP 都能跑), 也可以在openclaw.json里直接声明 MCP server。 - 当 server 暴露时:仓库实际实现的是 stdio transport(
src/mcp/tools-stdio-server.ts、openclaw-tools-serve.ts),目前对外只把cron等少数工具直接暴露给外部 MCP 客户端。 SSE/HTTP 路径走 plugin 扩展(如 acpx)。
MCP 工具到 LLM 眼里也只是 tools schema 里的一项。
特别的是 OpenClaw 把"绕路接 Codex / Claude / Cursor 生态"也包装成了 plugin bundle:
你可以把 Cursor 的 mcp.json 或 Claude Code 的 settings.json 整体作为 bundle 装进来,
OpenClaw 负责把里面的 tool 重映射成 native skill / hook / MCP(参考 docs plugins/bundles)。
#Plugin Tools(registerTool / registerService / registerCommand)
OpenClaw 还有一套 native plugin 体系(in-process、TypeScript)。一个 plugin 通过三个 注册原语暴露能力:
| 原语 | 用途 |
|---|---|
registerTool(name, schema, handler) |
注册一个新的 agent 工具(同 bundled tools 同构) |
registerService(...) |
后台长连接服务(比如订阅一个事件源、跑一个 watcher) |
registerCommand(name, handler) |
注册一个 slash command |
这是给"想深度集成"的开发者准备的。Plugin 的 hook token 跟 Gateway auth token 是
强制隔离的(hooks.token 必须不等于 auth.token)——这是个安全设计。
#4.2 工具选择:模型自己选 vs slash 命令显式触发
OpenClaw 给了用户两条路触发能力:
模型自动选: LLM 看到 tools 列表 → tool_call → 框架执行
用户显式触发: 用户输入 /<command> → Gateway hook → 直跑某个 skill / 工具
第二条路是 OpenClaw 比较有特色的设计——它支持 disable-model-invocation: true
的 skill:模型完全看不见,但用户可以通过 /skill-name args 主动触发。这把"日常对话
里不该被 LLM 自己滥用"和"我作为用户偶尔需要的开关"分得很清楚。
#4.3 Sandbox:Docker 隔离 + 工具 profile
Gateway 支持把 sub-agent / 不信任的 session 放进 Docker sandbox。工具白名单走两阶段
策略(tools.profile 选 preset → tools.alsoAllow / deny 微调,deny 优先):
sub-agent 默认 restriction layer(在 profile 之上再剥)
├─ 默认硬禁: sessions_list, sessions_history, sessions_send, sessions_spawn
│ (4 个 session 管理工具——见 docs/tools/subagents.md)
├─ 例外: maxSpawnDepth >= 2 时,第一层 orchestrator sub-agent 会拿回这 4 个 + subagents
└─ 其它工具: 按 profile 决定,可通过 alsoAllow 显式打开 / deny 显式关闭(deny 优先)
设计意图很直接:子代理默认不应该有"调度兄弟代理、读所有 session 历史"的能力,
否则一个被 prompt-injection 的子代理能在 agent 网络里横向移动。gateway / cron /
browser / canvas / nodes / web_* 不在硬禁名单里(profile 决定),但生产里通常会把
sub-agent 跑在 Docker sandbox + 裁剪过的工具集里。
#4.4 Slash Commands
Slash command 是用户输入面里类似 /help / list / focus / unfocus / agents / subagents list / verbose on / trace on 这种以 / 开头的指令,由 Gateway 在 hook 链早期识别并
分流(命令事件:command:new / command:reset / command:stop)。它们不进 LLM,
直接由 Gateway 处理,因此响应是确定性的、不消耗模型 token。
跟 Claude Code 的 .claude/commands/*.md(用户写一段 markdown 当成可重用 prompt)
机制不同:Claude Code 的 slash commands 实质是 prompt 模板;OpenClaw 的内置
slash commands 是 Gateway 命令。OpenClaw 的"prompt 模板"那一层一般用 SKILL 的
user-invocable: true 来达成。
#五、Agent 怎么记忆 —— MEMORY.md + 日记 + Active Memory 子代理
"长跑的助手最大的需求不是更快,是 '它还记得我上周说过我对花生过敏'。"
#5.1 三层记忆结构
┌─────────────────────────────┐
│ 当下 context(模型看见的那段)│
│ session.jsonl 滚动 transcript │
└────────────┬────────────────┘
│ 大了 → compact hook + memory flush hook
▼
┌──────────────────────────────┐
│ Compacted summary + 最近 N 轮 │
└──────────────┬───────────────┘
│
┌──────────────┼───────────────┐
▼ ▼
┌──────────────────────┐ ┌────────────────────────┐
│ MEMORY.md │ │ memory/YYYY-MM-DD.md │
│ "长期事实" │ │ "日记,按天分桩" │
│ 用户偏好/关键关系 │ │ 当天发生的事/决定/状态 │
│ 永久保留 │ │ 永久保留 │
└──────────────────────┘ └────────────────────────┘
▲
│ session 启动 / Active Memory 子代理 触发
│
┌──────────────┴───────────────┐
│ memory_search / memory_get │ ← 模型也能主动调
└──────────────────────────────┘
这是它"个人单机部署"哲学的直接体现:你的记忆不进任何外部数据库、永远在 workspace 目录里、可以直接 cat 出来读、可以 git 起来。
补充:DREAMS.md 与 dreaming sweep。
memory-host-sdk/dreaming.ts还提供了一种"梦境" 模式——agent 空闲时跑一轮 dream sweep,把当天的 memory 文件整理总结成DREAMS.md的一条新条目,供人类回看。这条线是社区项目yoloshii/ClawMem风格被吸收进 SDK 的结果,不属于核心写入路径。
可选:lancedb 向量记忆。除了 markdown 三层,OpenClaw 现在还内置
memory_recall工具(基于 lancedb)的可选路径——这是给"用户偏好之外、长文本/事件链"型记忆准备的。 默认 active-memory 子代理走memory_search/get(markdown 搜索);开 lancedb 时切到memory_recall。两条路径互斥,由 plugin 配置选择。
#5.2 写记忆:5 个 bundled hook(含 session-memory)
OpenClaw 自带 5 个 bundled hook(src/hooks/bundled/,含 README 描述):
| Hook | 监听事件 | 干什么 |
|---|---|---|
session-memory |
command:new / command:reset |
取最近 ~15 轮 user/assistant 消息,让 LLM 生成一个 slug,写到 <workspace>/memory/YYYY-MM-DD-HHMM-<slug>.md |
bootstrap-extra-files |
agent:bootstrap |
把额外的 AGENTS.md/TOOLS.md(如 monorepo 子目录)拼进 system prompt |
command-logger |
command(全部命令) |
追加 JSONL 到 ~/.openclaw/logs/commands.log |
compaction-notifier |
session:compact:before / session:compact:after |
在 compact 前后给用户/audit 总线发个通知 |
boot-md |
gateway:startup |
Gateway 启动后跑 workspace 的 BOOT.md 指令 |
注意触发时机的变化:早期版本里 session-memory 挂在 session:compact:before,
所以叫做"在 compact 之前抢救一次"。当前版本挂在 command:new / command:reset——
也就是说:是用户主动开新会话(/new)或重置(/reset)的时机才会把 transcript 蒸馏成
memory 文件。这是一个显式触发 vs 隐式抢救的语义切换:
session 内 transcript 增长 → 接近 context 上限
│
▼
Gateway 触发 hook: session:compact:before
│
▼
session-memory hook 启动一次 "silent agentic turn"
│ (这是一次额外的 LLM 调用,专门干"挑出哪些事实值得长期记"这件事)
│
├─ 读最近 transcript
├─ 提取关键事实 / 决定 / 用户偏好
└─ 把它们按性质 append 到:
├─ MEMORY.md(永久事实)
└─ memory/YYYY-MM-DD.md(当天发生的)
│
▼
然后 OpenClaw 才执行真正的 compact,缩短 transcript
│
▼
session:compact:after 触发
这个设计的意思是:/new 与 /reset 这两个用户行为本身就被默认视为"该把刚才那段
对话攒下来"的信号——用户只要愿意走流程,记忆就不会丢;如果用户从不重置而是无限聊下去,
compaction 仍由 compaction-notifier + compact 流程兜底,但 memory 文件的写入则只跟着
/new / /reset 走。
历史痛点(issue #56072 "Daily session reset silently discards context without memory flush"):早期版本的 daily reset 不走这个 flush,导致用户起床发现 agent 把 昨晚说的事全忘了。后来把 memory flush 拆成独立 hook(
session-memory)且让command:new也触发——这把"忘事"这个问题从"框架行为漏洞"转成了"用户行为"层面的契约。
#5.3 读记忆:Active Memory 子代理(启动前的"暗工序")
这是 OpenClaw 最有特色的一个机制。当你给 agent 发一条消息,在主 LLM 调用之前, Gateway 会起一个临时的 sub-agent——叫 active memory sub-agent——专门干一件事:
用户消息: "那个项目今天怎么样了?"
│
▼
active memory sub-agent (blocking, 只有 memory_search / memory_get 工具)
│
│ memory_search("项目状态 最近")
│ memory_get("MEMORY.md")
▼
返回 summary: "用户提到的项目应该是 'XX',上周决定走 A 方案,今天 deploy 计划在 16:00"
│
▼
这段 summary 作为 hidden <active_memory_plugin> 块拼到主 LLM 的 system 段
│
▼
主 agent 这才正式 infer,回答用户
关键参数(docs/concepts/active-memory):
| 配置 | 作用 |
|---|---|
queryMode |
message / recent / full —— 拿哪些上下文去搜记忆 |
promptStyle |
strict ↔ preference-only —— 子代理的"挑剔程度" |
timeoutMs |
硬超时(≤120s) |
maxSummaryChars |
注入到主 prompt 的字数上限 |
persistTranscripts |
默认 false(子代理 transcript 用完即删) |
- OpenClaw:用一个隐藏子代理在主调用前抢跑一遍,强制把记忆塞给主 agent
OpenClaw 的好处是用户感受不到延迟(与主调用并行/前置流水),坏处是多了一次 LLM 开销 和"agent 不知道自己看到的 context 来自哪里"的不可解释性。
#5.4 Compaction:手动 + 周期 + 阈值
OpenClaw 现在有三类 reset/compact 入口:
- Manual:
/new(开新 session)、/reset//reset soft [message](保留 transcript 软重置)、/compact [instructions](只 compact 不开新) - Idle reset:
session.idle配置;线程级可用/session idle <duration|off> - Hard max-age:
session.maxAge配置;线程级可用/session max-age <duration|off>
每次 reset / compact 期间会触发 session:compact:before → 模型驱动的 compact →
session:compact:after;compaction-notifier bundled hook 默认订这两个事件,把状态通知到
用户/audit。"默认每天 04:00 daily reset" 这一项已经在仓库里搜不到固定时间——
当前版本不强加默认 daily 时间,而是把节奏交给 session.maxAge / session.idle 这种参数
化配置。如果你的文档延用"04:00 默认 daily reset",需要核实你部署的具体版本。
派生命令:
/export-trajectory [path]把当前 session 导出成 trajectory bundle(JSONL, 见src/trajectory/export.ts),用于离线分析、问题复盘、或喂回 RL 训练(Gen-Verse/OpenClaw-RL)。
#六、Agent 怎么开口 —— Channels、Nodes、Canvas、WebSocket 事件总线
而是"通过 N 种 IM 通道、N 种节点设备、和一块 Canvas 跟人对话"。
#6.1 三种输出设备
┌──────────────────────────────┐
│ Channels(聊天通道) │
│ WhatsApp / iMessage / TG / │
│ Slack / Discord / Signal / │
│ Feishu / msteams / line / │ ← 主要输入也是它们
│ matrix / nostr / qqbot / │
│ tlon / twitch / synology-... │
│ 21 个仓库内置 adapter + │
│ WebChat(内置 web UI) │
└──────────────────────────────┘
┌──────────────────────────────┐
│ Nodes(设备节点) │
│ macOS 菜单栏 app │
│ iOS app(语音/位置/相机) │
│ Android app │
│ Headless 节点 │ ← Voice Wake / Talk Mode 都靠这层
│ macos-mlx-tts 节点(本地 TTS)│
└──────────────────────────────┘
┌──────────────────────────────┐
│ Canvas(视觉工作区) │
│ agent 可写的 HTML/CSS/JS │
│ 现走 Gateway 同端口能力路径 │ ← /__openclaw__/cap/<capId>/canvas/...
│ + A2UI(/__openclaw__/a2ui) │
│ Canvas 进程仍隔离 │
└──────────────────────────────┘
#6.2 Channels 适配——单向理解:channel adapter 是 plugin
每个 channel 是 extensions/<name> 下的一个 TypeScript 模块,挂着 defineBundledChannelEntry()
合约。它是一个独立的 WS 客户端,干两件事:
- 上行:从外部平台收消息 → 转换成内部 ChannelMessage → 走 Gateway 的 message hook 链
- 下行:从 Gateway 收 assistant payload → 转换成对应平台的格式 → 调外部 API 发出去
仓库内置 21 个 adapter(截 2026.05.21):
discord, feishu, googlechat, imessage, irc, line, matrix, mattermost,
msteams, nextcloud-talk, nostr, qqbot, signal, slack, synology-chat,
telegram, tlon, twitch, whatsapp, zalo, zalouser
README 公开口径常说"22+/23 通道"——多出来的是 WebChat(内置 web UI,不在 extensions/)、
以及配套的 Voice/Phone 通道(属于 Talk 系统,见 §0.6 src/talk/)。
不同平台的现实差异巨大:
- Telegram 走
grammy1.42 - WhatsApp 走 Baileys 7.x(多账号实例非官方协议)
- iMessage 走 BlueBubbles 或 macOS 私有桥(仓库内是
imessageadapter + skillimsg) - Discord 用
discord-api-types+ 原生ws - Signal 走
signal-cliHTTP 桥 - msteams(Microsoft Teams)、googlechat、feishu 是各家官方 bot API
- WebChat 是 OpenClaw 自己提供的 web UI(在
ui/、apps/swabble/)
但上行/下行的内部表达是统一的——这点跟 ai-gateway "把 LLM 厂商抽象成 OpenAI schema"是同一个套路:抽象差异、暴露统一接口。
Discord 线程绑定(2026 Q1 新增):
/focus <thread>//unfocus把某个线程绑成 当前 channel 的"主话题",配合commands.enforceOwnerForCommands做 per-channel owner 校验。这套机制只在 Discord channel 完整支持,配套配置在session.threadBindings。
#6.3 Nodes 适配——把手机当感官
Nodes 是另一类 WS 客户端,但 handshake 时 role: "node",并声明自己拥有的 caps:
// 一台 iOS 节点连上来时的 connect 帧(简化)
{
"role": "node",
"deviceId": "iphone-15-pro",
"caps": {
"voiceWake": true,
"talkMode": true,
"camera": true,
"location": true,
"canvasRender": true
},
"permissions": ["mic", "location"],
"commands": ["camera_capture", "speak", "wake_listen", "location_get"]
}
主 agent 想用手机的某个能力时,调 nodes bundled 工具:
LLM tool_call: nodes.invoke({nodeId: "iphone-15-pro", command: "camera_capture", args: {...}})
│
▼
Gateway 找到对应的 WS 节点连接 → 发请求 → 等响应 → 把图片转给 agent
这就是为什么 OpenClaw 能做 Voice Wake / Talk Mode / 实时拍照之类的事—— 不是 Gateway 自己干的,而是它控制了你手机这个节点。
#6.4 Canvas——agent 的"画板"输出
Canvas 是 OpenClaw 给 agent 一块可写的 HTML/CSS/JS 工作区。Canvas 进程仍然独立运行 (隔离崩溃 / 严格沙箱),但对外不再绑独立端口 18793——访问入口收敛到 Gateway 本身:
路由形态(截 2026.05)
Gateway 端口(默认 18789)
├─ /__openclaw__/canvas/... ← docs.openclaw.ai 公开口径的 canvas 根路径
├─ /__openclaw__/cap/<capId>/canvas/... ← 源码里 per-capability token 的细分路径(待核实)
└─ /__openclaw__/a2ui/... ← agent-to-UI 协议接口
docs.openclaw.ai/concepts/architecture 原文只说 "served under /__openclaw__/canvas/
and /__openclaw__/a2ui/,using the same port as the Gateway";社区代码里能看到
/__openclaw__/cap/<capId>/canvas/... 形态的 per-capability token 路径(每个 Canvas
文档/会话拿一段独立 token,跟 Gateway auth 分离),这一层是否在主仓 v2026.5.20 默认开启
待核实。无论哪种路径形态,设计意图都是:把"Canvas 内容是 agent 写的"的安全边界跟
"Gateway 是 user 控制面"的安全边界用 URL 路径而不是端口隔开。
agent 通过 canvas skill / a2ui 协议操作(创建页面、注入数据、监听用户点击)。
用户用 macOS app 或浏览器看 Canvas。
#6.5 WebSocket 事件总线(内部协议)
Gateway 与 channels / nodes / control-plane(macOS app, CLI, web UI)之间全部都是 WebSocket,统一帧格式:
连接: {role, deviceId/clientId, caps, auth}
请求: {type: "req", id, method, params}
响应: {type: "res", id, ok, payload | error}
事件(推送): {type: "event", event, payload}
事件流类型(节选):
| 事件 | 谁推 | 谁订 |
|---|---|---|
agent:lifecycle |
Gateway | macOS app(看运行状态)/ web UI |
agent:assistant:delta |
Agent loop | Channel adapter(实时打字效果)/ control-plane |
agent:tool:start/end |
Agent loop | macOS app(实时显示在干啥) |
chat:received |
Channel adapter | 其它订阅者 |
presence |
Gateway | 同一用户的多客户端互相知道在线状态 |
cron:fired |
Gateway | Agent(醒来执行定时任务) |
health / heartbeat |
各方 | 监控 |
通信,没有 Kafka,没有 Redis pub/sub**。代价:所有连接都依赖 Gateway 进程在线。
#6.6 Cron——agent 的"闹钟"
cron bundled tool 让 agent 给自己设定时任务:
LLM tool_call: cron.add({schedule: "0 9 * * MON", action: "wake", payload: "周一早会准备"})
│
▼
Gateway 把这条 cron 持久化(在 workspace 下)
│
▼
到点 Gateway emit cron:fired → 起一个 Run,把 payload 当成新 user message 灌进去
这就是 OpenClaw 实现 "24×7 主动叫你" 的物理基础。Cron 触发的 Run 跟 IM 进来的 Run 一样走完整 hook 链 + agent loop。
#七、Agent 的外脑 —— 模型无关层与 OpenRouter / iblai-router 接入
"OpenClaw 不写自己的 ai-gateway。它把 model provider 这事做得极简:"你给我一个 兼容 OpenAI 或 Anthropic 协议的 endpoint 就行。"
#7.1 配置面:openclaw.json 里的 models 段
{
"models": {
"default": "openrouter/anthropic/claude-sonnet-4.5",
"providers": {
"openrouter": {
"baseUrl": "https://openrouter.ai/api/v1",
"api": "openai-chat",
"apiKey": "${OPENROUTER_API_KEY}"
},
"iblai-router": {
"baseUrl": "http://127.0.0.1:8402",
"api": "anthropic-messages",
"apiKey": "passthrough",
"models": [
{"id": "auto", "contextWindow": 200000, "maxTokens": 8192, "reasoning": true}
]
},
"ollama": {
"baseUrl": "http://127.0.0.1:11434/v1",
"api": "openai-chat"
}
}
}
}
模型 ref 的统一格式:<provider>/<model-id> 或者 <provider>/<sub-provider>/<model>。
比如 openrouter/anthropic/claude-sonnet-4.5、ollama/qwen3:32b。
#7.2 它没做什么
OpenClaw 这一层故意做得很瘦:
| 你可能期待的能力 | OpenClaw 做不做 |
|---|---|
| 多 key 池 | 不做(让 OpenRouter / 路由器去做) |
| 厂商间 failover | 不做 |
| 模型组(weighted/random) | 不做(让外部 router 做) |
| 自动 token 计费/配额 | 部分做(统计每次 Run 的 tokens,但不是计费系统) |
| Prompt cache | 透传(取决于 provider) |
| 协议归一 | 做:内部用 OpenAI tools schema,对接 anthropic-messages 时由 provider 适配 |
它把"路由复杂性"全部外包给 OpenRouter 或 iblai-router(社区项目,号称 41+ 模型、 USDC 计费)这种第三方路由器。这是生态分工的设计选择。
仓库内置的 provider 扩展(截 2026.05.21)——
- LLM:
anthropic/anthropic-vertex/openai/openrouter/ollama/groq/cerebras/mistral(社区/插件)/deepseek/deepinfra/fireworks/huggingface/byteplus/alibaba/arcee/chutes/cloudflare-ai-gateway/copilot-proxy/github-copilot/codex(OpenAI subscription)/azure-speech(既 有 speech 也有 LLM 部分)- Speech / TTS / STT:
elevenlabs/deepgram/azure-speech(加上 skills 里的openai-whisper、openai-whisper-api、sherpa-onnx-tts)- 媒体生成:
fal/comfy等通过image-generation-core接入;视频/音乐生成各自 有 provider registry- 特化:
amazon-bedrock/amazon-bedrock-mantle、google-meet(不是 LLM 而是 会议数据接入)、bonjour(本机服务发现)这套清单解释了为什么
extensions/下有 130+ 个目录——其中 21 个是 channel,剩下 大部分是 provider/能力扩展,再加几个 hook 包(active-memory、diagnostics-otel、diagnostics-prometheus、device-pair、diffs、document-extract、firecrawl、exa、brave、duckduckgo、file-transfer、canvas、browser等)。
#7.3 调用链(极简)
Agent loop
│ model.infer(messages, tools, stream=true)
│
▼
ModelProviderRegistry.resolve("openrouter/anthropic/claude-sonnet-4.5")
│
├─ provider = providers["openrouter"]
├─ realModel = "anthropic/claude-sonnet-4.5"
└─ apiKey = provider.apiKey
│
▼
OpenAIAdapter.chat({...}) → POST https://openrouter.ai/api/v1/chat/completions
│
▼
SSE 流式响应 → 解析为 RunEvent (assistant deltas + tool_calls)
- 配额 + 计费的路子完全相反——OpenClaw 是单用户,不需要这些。
#八、关键设计权衡(架构师视角)
把前面所有章节的"为什么"提炼出来,形成可复用的设计原则:
#8.1 一个 Gateway 进程 vs 一组微服务
问题:调度、推送、聚合、IM 适配、鉴权,都要做。要拆几个服务? 回答:单进程 daemon。所有事都压在 Gateway 里。
复用启发:跟用户量级匹配的拓扑才合理。OpenClaw 是单机单用户,没有跨节点协作需求, "分布式"的优势完全不适用,反而带来部署复杂度。先看物理拓扑再设计软件拓扑。
#8.2 Run 串行 + Sub-Agent 显式并发
问题:用户在 agent 还没回完时连发三句怎么办? 回答:session lane 锁串行排队;要并发就显式 spawn sub-agent。
复用启发:在"transcript append-only"的世界里,串行是对的——并发写一份历史 就是在制造数据竞争。如果你的系统也是"以 log 为真",先锁后并发。
#8.3 SOUL/Skill 分别可发布
问题:社区生态里"人格"和"能力"应该耦合还是分离? 回答:分开。SOUL = 人格(带 lobster-mom 也带 strict-cto),SKILL = 能力。 一个 agent = workspace + 1 个 SOUL + N 个 SKILL。
复用启发:当你想做"乐高式"插件生态,分清楚"风格"和"功能"。它们的更新频率、 验证方式、发布权限都不一样——硬绑在一起会让生态僵硬。
#8.4 Active Memory 子代理 vs 主 agent 自查记忆
问题:长期记忆怎么进 context? 回答:开一个隐藏子代理在主 LLM 调用前先查一遍 → summary 注入。
复用启发:当你不信任主 LLM 会"自己想起来去查记忆"时,用子代理强制前置。 代价是多一次 LLM 调用、多一份不可解释性。适合"用户隐性偏好/长期事实"型记忆, 不适合"很大量、很具体"的知识库(那种该走 RAG 工具)。
#8.5 hook 链作为软件总线
问题:要在消息处理链路里塞 N 种切面(鉴权、审计、记忆、压缩、自定义)怎么办? 回答:定义一组离散事件名(
message:received/command:new/session:compact:before…), 让 hook 监听+拦截,每个 hook 是HOOK.md + handler.ts。
复用启发:hook 本质上是事件溯源的软件总线。当你的系统横切关注点很多、又想让 社区扩展时,hook 比"中间件链"更可发布、更模块化。代价:调试链路变长(要追"事件 谁触发的、谁吃的、谁改的")。
#8.6 hook token 与 gateway token 隔离
问题:插件能调 Gateway,那它会不会能干所有事? 回答:
hooks.token必须 ≠auth.token。Gateway 拒绝两个值相同。
复用启发:最小权限原则的工程化。两个权限边界就强制两个凭证,连配置层都不允许 重用。这种"宁愿在配置 schema 上多一行约束,也不让用户偷懒"的取舍很有借鉴意义。
#8.7 Skill 6 层加载优先级
问题:bundled / managed / personal / project / workspace 同名 skill 怎么办? 回答:定义清晰的 6 层优先级,高的覆盖低的,session 启动时打快照。
复用启发:任何"配置可叠加"的系统都需要明确的优先级表 + 快照时机。优先级是 覆盖语义;快照时机决定"运行中改配置算不算数"。两者都不写清楚就会变成 bug 来源。
#8.8 SKILL.md 把"环境依赖"写进 frontmatter
问题:用户装了一堆社区 skill,但不是每个 skill 都能在他的机器上跑起来。 回答:每个 skill 在 frontmatter 声明
requires.env / bins / config / os, session 启动时自动 gating 过滤。
复用启发:让运行依赖前置可声明。比起"运行时报错","加载时静默跳过 + 一条 诊断"对用户友好得多。所有"插件市场"型项目都该学这一招。
#8.9 session reset + memory flush 抢救
问题:长跑 agent 的 context 会无界增长,必须 reset。但 reset 又会丢上下文。 回答:把"reset 时机"参数化(
session.idle/session.maxAge+ 用户的/new/reset/compact),并把"reset 前先写 MEMORY/日记"做成独立的session-memorybundled hook(监听command:new/command:reset,不再绑死凌晨 04:00——早期版本 那个固定时间在当前源码已查不到)。
复用启发:任何"周期性 GC"的系统都要配套"GC 前抢救机制"。GC 本身是简单的,
难的是确保 GC 不丢业务关心的状态。这也是为什么 OpenClaw 的 5 个 bundled hook 里
专门有一个就叫 session-memory。
#九、附录:组件地图、关键文件、源码索引
#9.1 组件地图(一页式,对齐 v2026.5.20)
| 圈层 | 组件 | 形态 | 职责 | 关键依赖 |
|---|---|---|---|---|
| 入口 | Channel adapters (21 个仓库内置) | TS, WS client | WhatsApp/Telegram/Slack/iMessage/msteams/feishu/... | grammY 1.42 / Baileys 7 / 各家 SDK |
| 入口 | Nodes (iOS / Android / macOS / macos-mlx-tts) | 原生 app, WS client | 语音、相机、位置、Canvas 渲染、本地 TTS | apps/{ios,android,macos} + OpenClawKit |
| 入口 | Control-plane (macOS app / CLI / web UI / swabble) | 各种 client | 用户操作 agent 的 UI | WS / HTTP |
| 入口 | Talk / RealtimeTranscription | TS in-proc | 实时电话/麦克风通道 | src/talk/、src/realtime-transcription/ |
| 核心 | Gateway daemon | TS, 单进程 18789 | 路由、调度、hook、Cron、Canvas hosting、cap 路由 | 文件状态、WS+HTTP server |
| 核心 | Agent harness (pi-embedded-runner) | TS in-proc | 单 session 单 Run 的 ReAct 循环 + context-engine 钩子 | model provider |
| 核心 | Context engine | TS in-proc | "system prompt 该塞啥"的 delegate + registry | src/context-engine/ |
| 核心 | Commitments / Tasks / Auto-reply | TS in-proc | 承诺跟踪、detached task、自动回复 | src/commitments/、src/tasks/、src/auto-reply/ |
| 能力 | Bundled tools (~25) | TS in-proc | sessions_* / web_fetch / web_search / image_generate / music_generate / video_generate / tts / pdf / nodes / cron / memory_* / update_plan / ... | 各类 native API |
| 能力 | Skills (5400+ ClawHub, 仓库内 53) | markdown + 资源 | "agent 能干啥"的开放生态 | clawhub registry |
| 能力 | MCP(host & server) | stdio in repo;SSE/HTTP via plugin | 第三方协议接入 + OpenClaw 工具对外暴露 | MCP spec、mcporter skill |
| 能力 | Plugins | TS in-proc | registerTool 主路径 + defineBundledChannelEntry + plugin-state | packages/plugin-sdk/、packages/plugin-package-contract/ |
| 记忆 | MEMORY.md / memory/ / DREAMS.md | 文件 | 长期事实 + 日记 + 梦境总结 | — |
| 记忆 | session-memory hook (bundled) | hook | /new / /reset 时把最近转写蒸馏写入 memory/YYYY-MM-DD-HHMM-<slug>.md |
LLM (silent turn) + LLM slug 生成 |
| 记忆 | Active memory sub-agent | 隐藏 sub-agent | 主调用前查记忆 | memory_search/get/recall(含 lancedb 选项) |
| 记忆 | Memory host SDK + dreaming | engine + sweep | 把记忆从 markdown 扩成可被第三方 host 的 engine | packages/memory-host-sdk/、src/memory-host-sdk/ |
| 外脑 | Model provider adapter | TS in-proc | 接 OpenRouter / Ollama / 各家 LLM / TTS / STT | OpenAI / Anthropic / 各家 API |
| 外脑 | Model catalog + provider-runtime | TS in-proc | 模型清单/定价/能力 + retry/circuit | src/model-catalog/、src/provider-runtime/ |
| 可视 | Canvas + A2UI | 隔离进程 + Gateway 同端口路径 | agent 写的 HTML/CSS/JS | /__openclaw__/canvas/*、/__openclaw__/a2ui/*(社区代码可见 /__openclaw__/cap/<id>/canvas/... 形态,待核实) |
| 审计 | Crestodian / Trajectory | TS in-proc | dialogue/audit、JSONL 轨迹导出 | src/crestodian/、src/trajectory/ |
| 调试 | Proxy-capture (CA + proxy) | TS in-proc | agent 外呼网络流量自检 | src/proxy-capture/ |
#9.2 关键概念表(中英对照速查)
| 概念 | 英文 | 一句话 |
|---|---|---|
| 网关 | Gateway | 单机的 daemon,所有 IO 的总线 |
| 工作区 | Workspace | 一个 agent 的私人办公室目录 |
| 灵魂 | SOUL.md | 可发布的人格(来自 ClawHub) |
| 工作手册 | AGENTS.md | 多 agent 协作的根规则(Claude Code 兼容 CLAUDE.md) |
| 工具说明 | TOOLS.md | 当前 workspace 的工具用法 |
| 身份 | IDENTITY.md | 名字/emoji/avatar |
| 技能 | SKILL.md | 一个能力的最小可发布单元,frontmatter + body |
| 钩子 | HOOK.md + handler.ts | 监听 Gateway 事件、拦截/改写 |
| 节点 | Node | 连上 Gateway 的设备(iOS/Android/macOS/headless) |
| 通道 | Channel | 连上 Gateway 的 IM 适配(Telegram/Slack/...) |
| 画布 | Canvas | agent 可写的 HTML/JS 工作区 |
| 会话车道锁 | Session lane | 同 session key 内 Run 串行的锁 |
| 主动记忆 | Active Memory | 主 LLM 调用前的记忆抢跑子代理 |
| 子代理 | Sub-agent | 父 Run 显式 spawn 出来的并行 Run |
| 模型路由 | Model providers | openclaw.json 里的 provider 表 + <provider>/<model> ref |
#9.3 关键数据结构(节选)
#openclaw.json(中央配置)
{
"agents": {
"list": [{ "id", "workspace", "skills?", "model?" }],
"defaults": { "skills": [...] }
},
"bindings": [
{ "channel", "accountId?", "agentId" }
],
"models": {
"default": "<provider>/<model>",
"providers": { "<name>": { "baseUrl", "api", "apiKey", "models?" } }
},
"skills": { "load": { "extraDirs": [...] } },
"hooks": {
"internal": { "entries": { ... } },
"token": "...", // ≠ auth.token
"wake": { ... }
},
"auth": { "token": "..." }
}
#Workspace 结构
<workspace>/
├── IDENTITY.md / SOUL.md / AGENTS.md / TOOLS.md
├── MEMORY.md
├── memory/YYYY-MM-DD.md
├── skills/<name>/SKILL.md
├── sessions/<sessionKey>.jsonl
└── .agents/skills/... ← project-scoped
#Session key 格式
agent:<agentId>:<channel>:<chatId> ← 主 session
agent:<agentId>:subagent:<uuid> ← 子代理
agent:<agentId>:subagent:<u1>:subagent:<u2> ← 嵌套子代理
#9.4 源码 / 文档索引(对齐 v2026.5.20)
行号无法稳定核实,以下仅给到文件级。需要更精确的位置请直接用仓库搜索。
| 关注点 | 仓库路径 |
|---|---|
| 项目入口与总览 | openclaw/openclaw 仓库 README + AGENTS.md + VISION.md |
| CLI 入口 | openclaw.mjs、src/entry.ts、src/cli/program/register.*.ts |
| Bootstrap / Wizard | src/bootstrap/、src/wizard/setup.*.ts |
| Agent loop 文档 | docs/concepts/agent-loop.md |
| Agent harness 源码 | src/agents/pi-embedded-runner/、src/agents/harness/ |
| Gateway 架构文档 | docs/concepts/architecture.md |
| Gateway 服务器实现 | src/gateway/server.ts、server-http.ts、server-ws-runtime.ts、server-chat.ts |
| Gateway 协议 / handshake | src/gateway/handshake-timeouts.ts、src/daemon/ |
| Session lane / 并发控制 | src/process/lanes.ts、src/agents/pi-embedded-runner/lanes.ts、src/infra/heartbeat-runner.ts |
| Skills 文档 | docs/tools/skills.md |
| Skills 内置目录 | skills/(53 个) |
| Sub-agents 文档 | docs/tools/subagents.md |
| Slash commands 文档 | docs/tools/slash-commands.md |
| Hooks 文档 / 实现 | docs/automation/hooks.md、src/hooks/、bundled: src/hooks/bundled/{session-memory,bootstrap-extra-files,command-logger,compaction-notifier,boot-md}/ |
| Hook 事件类型 | src/hooks/internal-hook-types.ts、src/hooks/types.ts |
| Plugin bundles 文档 | docs/plugins/bundles.md |
| Plugin SDK | packages/plugin-sdk/、packages/plugin-package-contract/、src/plugin-sdk/、src/plugins/ |
| Plugin 状态持久化 | src/plugin-state/plugin-state-store.ts |
| Active memory 文档 / 源码 | docs/concepts/active-memory.md、extensions/active-memory/index.ts |
| Memory host SDK & dreaming | packages/memory-host-sdk/、src/memory-host-sdk/{engine-storage,dreaming}.ts |
| Model providers 文档 | docs/concepts/model-providers.md |
| Model catalog / provider-runtime | src/model-catalog/manifest-planner.ts、src/provider-runtime/operation-retry.ts |
| OpenRouter 接入 | docs/providers/openrouter.md |
| Channel 适配集 | extensions/{discord,feishu,googlechat,imessage,irc,line,matrix,mattermost,msteams,nextcloud-talk,nostr,qqbot,signal,slack,synology-chat,telegram,tlon,twitch,whatsapp,zalo,zalouser}/ |
| Bundled tools | src/agents/tools/{sessions-*,web-*,image-*,music-*,video-*,tts-*,pdf-*,nodes-*,cron-*,update-plan-*,message-*,heartbeat-response-*,session-status-*,agents-list-*,gateway-*,subagents-*}.ts |
| MCP 服务端 | src/mcp/tools-stdio-server.ts、src/mcp/openclaw-tools-serve.ts |
| Canvas / A2UI | ui/src/ui/canvas-url.ts、extensions/canvas/(docs 公开路径 /__openclaw__/canvas、/__openclaw__/a2ui;社区代码可见 /__openclaw__/cap/<id>/canvas 细分形态,待核实) |
| Cron 服务 | src/cron/{types,service-contract,isolated-agent}.ts |
| Talk(实时语音) | src/talk/{provider-types,agent-talkback-runtime}.ts |
| Realtime STT | src/realtime-transcription/provider-registry.ts |
| 媒体生成 | src/{image-generation,music-generation,video-generation,tts}/provider-registry.ts |
| Tasks / Commitments / Auto-reply | src/tasks/、src/commitments/、src/auto-reply/ |
| Context engine | src/context-engine/{delegate,registry,types}.ts |
| Trajectory 导出 | src/trajectory/export.ts(命令 /export-trajectory) |
| Crestodian audit | src/crestodian/{dialogue,audit}.ts |
| Proxy capture | src/proxy-capture/{proxy-server,ca}.ts |
| Pairing / Secrets / Status | src/pairing/、src/secrets/plan.ts、src/status/ |
| Node host policy | src/node-host/{exec-policy,invoke-system-run}.ts |
| ACP(Agent Control Protocol) | src/acp/{types,persistent-bindings}.ts、extensions/acpx/ |
| 配套 / 节点 app | apps/{android,ios,macos,macos-mlx-tts,shared,swabble}/(共享库:OpenClawKit) |
| Skill 格式规范 | openclaw/clawhub 仓库 docs/skill-format.md |
| Soul 格式规范 | openclaw/clawhub 仓库 docs/soul-format.md |
| ClawHub 注册表 | openclaw/clawhub(社区 skills / souls) |
| 第三方 LLM 路由 | BlockRunAI/ClawRouter、iblai/iblai-openclaw-router |
| 持久记忆插件 | yoloshii/ClawMem(hooks + MCP + 混合 RAG) |
| RL 衍生 | Gen-Verse/OpenClaw-RL |
| awesome-skills | VoltAgent/awesome-openclaw-skills(5,400+ 社区 skill 筛选) |
| Mission Control(外部 dashboard) | abhi1693/openclaw-mission-control |
#9.5 一句话总结(贴在脑子里)
OpenClaw 是一个把"个人 IM 助手"做到极致的、跑在你自己机器上的单进程 agent 框架。 Gateway 是它唯一的中枢,channels 是它的耳朵,nodes 是它的手脚, Canvas 是它的画板,SOUL/SKILL 是社区给它换上的灵魂和技能,hook 链是它的事件总线, markdown 文件就是它的长期记忆。
整个系统的灵魂在三个设计上:Gateway 单进程吃下所有 IO(与"分布式 agent" 旗帜性区分)、SOUL/SKILL 双轨可发布生态(让人格和能力解耦演化)、 Active Memory 隐藏子代理 + bundled session-memory hook(让长跑助手真的记得你)。
数据来源(2026.05.21 复核):OpenClaw 官方仓库
openclaw/openclaw@main(v2026.5.20)、docs.openclaw.ai、openclaw.ai/blog,以及 ClawHub 规范文档 (github.com/openclaw/clawhub/docs/),均为公开资料。本版用 WebFetch 直读了package.json、docs/concepts/{agent-loop,architecture,agent-workspace,active-memory}、docs/automation/hooks、docs/tools/{skills,subagents}等页面对位。本版核实记录:
- ✅
package.jsonversion 为2026.5.20(raw.githubusercontent.com 直读)- ✅ 官方
docs/concepts/agent-workspace列出的 workspace 根 markdown 共 9 个: IDENTITY / SOUL / AGENTS / TOOLS / USER / MEMORY / HEARTBEAT / BOOT / BOOTSTRAP。DREAMS.md不在官方列表——已在 §1.1 标"非官方/SDK 派生";拼接顺序未文档化, 按src/agents/harness/实现推断- ✅ Active Memory 是同步阻塞 sub-agent(默认
timeoutMs=15000、maxSummaryChars=220、queryMode="recent"、promptStyle="balanced"、persistTranscripts=false)- ✅ Bundled hook 实际 5 个:
session-memory(command:new/command:reset) /bootstrap-extra-files(agent:bootstrap) /command-logger(command) /compaction-notifier(session:compact:before/:after) /boot-md(gateway:startup)- ✅ Gateway 默认端口
127.0.0.1:18789;docs 公开口径 Canvas 路径/__openclaw__/canvas/+/__openclaw__/a2ui/,与 Gateway 同端口(不再 18793)- ✅ Sub-agent 默认 deny 只有 4 个:
sessions_list/sessions_history/sessions_send/sessions_spawn;maxSpawnDepth >= 2的 orchestrator 会拿回这 4 个 +subagents- ✅ Skill 6 层加载优先级 + session 启动打快照 + watcher 触发 mid-session 刷新 (
docs/tools/skills.md原文一致)- ⚠ Canvas per-capability token 路径
/__openclaw__/cap/<id>/canvas/...在 docs 公开口径 里未出现——可能是源码细节 / 未升入文档(待核实)- ⚠ "默认每天 04:00 daily reset" 在当前源码 / docs 都查不到固定值——已统一改成
session.idle/session.maxAge参数化口径- ⚠ Channel 总数:仓库内置
extensions/21 个(不含 WebChat / Voice / Phone),README 外部口径 "22+/23" 包括了 WebChat 和 Talk 通道- ⚠ Agent harness 内部路径
src/agents/pi-embedded-runner//src/agents/harness//src/process/lanes.ts等源码位置基于 README 描述 + WebFetch 推断,未逐文件核实