openclacky 0.7.5 → 0.7.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.clackyrules +13 -1
- data/CHANGELOG.md +46 -0
- data/docs/memory-architecture.md +343 -0
- data/lib/clacky/agent/skill_manager.rb +8 -4
- data/lib/clacky/agent/system_prompt_builder.rb +1 -1
- data/lib/clacky/agent/tool_executor.rb +15 -6
- data/lib/clacky/agent.rb +27 -4
- data/lib/clacky/agent_config.rb +2 -2
- data/lib/clacky/cli.rb +69 -1
- data/lib/clacky/default_skills/create-task/SKILL.md +102 -0
- data/lib/clacky/json_ui_controller.rb +1 -5
- data/lib/clacky/plain_ui_controller.rb +143 -0
- data/lib/clacky/server/http_server.rb +731 -0
- data/lib/clacky/server/scheduler.rb +246 -0
- data/lib/clacky/server/session_registry.rb +122 -0
- data/lib/clacky/server/web_ui_controller.rb +211 -0
- data/lib/clacky/skill.rb +5 -2
- data/lib/clacky/tools/edit.rb +14 -145
- data/lib/clacky/ui2/components/message_component.rb +22 -1
- data/lib/clacky/ui2/layout_manager.rb +82 -20
- data/lib/clacky/ui2/ui_controller.rb +76 -30
- data/lib/clacky/ui2/view_renderer.rb +4 -2
- data/lib/clacky/ui_interface.rb +1 -2
- data/lib/clacky/utils/file_ignore_helper.rb +8 -0
- data/lib/clacky/utils/string_matcher.rb +136 -0
- data/lib/clacky/version.rb +1 -1
- data/lib/clacky/web/app.css +785 -0
- data/lib/clacky/web/app.js +351 -0
- data/lib/clacky/web/index.html +127 -0
- data/lib/clacky/web/sessions.js +237 -0
- data/lib/clacky/web/settings.js +283 -0
- data/lib/clacky/web/tasks.js +290 -0
- data/lib/clacky/web/ws.js +119 -0
- data/lib/clacky.rb +3 -0
- metadata +44 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5c3815310a11bea9ccad7ab6891b4ad85e248635af6919606e8963096699a1d7
|
|
4
|
+
data.tar.gz: 17f8059179701394a6951966ec0284ee6321750e2651c4de0ecbf37a4542c7fc
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b7b32ce62e12f9aa12db59b1f6640e4a6234c34c485d9b679df80654dc8722a369687e1fc0b5165a78a12f6210fcd02959133d6d3753c5c22873d071251b07fb
|
|
7
|
+
data.tar.gz: 20f4498c976579333b7e261490d287a33cec89b888c5ae1a640205c25f48c70c3863a551e1bef6173804b7670970ead88ed9a67702987da97963c0a981cd1e2a
|
data/.clackyrules
CHANGED
|
@@ -49,7 +49,11 @@ It provides chat functionality and autonomous AI agent capabilities with tool us
|
|
|
49
49
|
- **IMPORTANT**: When testing clacky commands or debugging, always use `bundle exec ruby bin/clacky` instead of the global `clacky` command. The global command loads the system-installed gem version (e.g., `openclacky-0.7.0`), not your local development code
|
|
50
50
|
|
|
51
51
|
### Tool Development
|
|
52
|
-
|
|
52
|
+
**IMPORTANT**: Do NOT add new Ruby Tool classes without careful consideration. Tools are low-level primitives (file I/O, shell, web search, etc.). Before creating a new Tool, always ask: can this capability be achieved with an existing Tool + a Skill (SKILL.md) instead? If yes, use a Skill.
|
|
53
|
+
|
|
54
|
+
Only add a new Tool when it requires capabilities that no existing tool can provide (e.g., a new API integration, a new system-level operation).
|
|
55
|
+
|
|
56
|
+
When a new Tool is truly necessary:
|
|
53
57
|
1. Create class in `lib/clacky/tools/`
|
|
54
58
|
2. Inherit from `Clacky::Tools::Base`
|
|
55
59
|
3. Define required class attributes
|
|
@@ -57,6 +61,14 @@ When adding new tools:
|
|
|
57
61
|
5. Add optional `format_call` and `format_result` methods
|
|
58
62
|
6. Require in `lib/clacky.rb`
|
|
59
63
|
|
|
64
|
+
### Skill Development
|
|
65
|
+
Skills are the preferred way to add new high-level capabilities. A skill is a Markdown instruction file (SKILL.md) that guides the Agent to accomplish a goal using existing Tools.
|
|
66
|
+
|
|
67
|
+
- Built-in default skills live in `lib/clacky/default_skills/<skill-name>/SKILL.md` (shipped with the gem)
|
|
68
|
+
- Project-level skills live in `.clacky/skills/<skill-name>/SKILL.md`
|
|
69
|
+
- User-level skills live in `~/.clacky/skills/<skill-name>/SKILL.md`
|
|
70
|
+
- **Prefer Skills over new Tools** whenever the task can be composed from existing primitives (write, shell, read, etc.)
|
|
71
|
+
|
|
60
72
|
### Agent Behavior
|
|
61
73
|
- TODO manager is for planning only - must execute tasks after planning
|
|
62
74
|
- Always use tools to create/modify files, don't just return code
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,52 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.7.7] - 2026-03-04
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- Web UI server with WebSocket support for real-time agent interaction in the browser (`clacky serve`)
|
|
14
|
+
- Task scheduler with cron-based automation, REST API, and scheduled task execution
|
|
15
|
+
- Settings panel in web UI for viewing and editing AI model configurations (API keys, base URL, provider presets)
|
|
16
|
+
- Image upload support in web UI with attach button for multimodal prompts
|
|
17
|
+
- Create Task button in the task list panel for quick task creation from the web UI
|
|
18
|
+
- `create-task` default skill for guided automated task creation
|
|
19
|
+
|
|
20
|
+
### Improved
|
|
21
|
+
- Web UI frontend split into modular files (`ws.js`, `sessions.js`, `tasks.js`, `settings.js`) for maintainability
|
|
22
|
+
- Web session agents now run in `auto_approve` mode for unattended execution
|
|
23
|
+
- Session management moved to client-side for faster, round-trip-free navigation
|
|
24
|
+
- User message rendering moved to the UI layer for cleaner architecture
|
|
25
|
+
- No-cache headers for static file serving to ensure fresh asset delivery
|
|
26
|
+
|
|
27
|
+
### Fixed
|
|
28
|
+
- `DELETE`/`PUT`/`PATCH` HTTP methods now supported via custom WEBrick servlet
|
|
29
|
+
- Task run broadcasts correctly after WebSocket subscription; table button visibility fixed
|
|
30
|
+
- Mutex deadlock in scheduler `stop` method when called from a signal trap context
|
|
31
|
+
- `split` used instead of `shellsplit` for skill arguments to avoid parsing errors
|
|
32
|
+
|
|
33
|
+
### More
|
|
34
|
+
- Add HTTP server spec and scheduler spec with full test coverage
|
|
35
|
+
- Minor web UI style improvements and reduced mouse dependency
|
|
36
|
+
|
|
37
|
+
## [0.7.6] - 2026-03-02
|
|
38
|
+
|
|
39
|
+
### Added
|
|
40
|
+
- Non-interactive `--message`/`-m` CLI mode for scripting and automation (run a single prompt and exit)
|
|
41
|
+
- Real-time refresh and thread-safety improvements to fullscreen UI mode
|
|
42
|
+
|
|
43
|
+
### Improved
|
|
44
|
+
- Extract string matching logic into `Utils::StringMatcher` for cleaner, reusable edit diffing
|
|
45
|
+
- Glob tool now uses force mode in system prompt for more reliable file discovery
|
|
46
|
+
- VCS directories (`.git`, `.svn`, etc.) defined as `ALWAYS_IGNORED_DIRS` constant
|
|
47
|
+
|
|
48
|
+
### Fixed
|
|
49
|
+
- Subagent fork now injects assistant acknowledgment to fix conversation structure issues
|
|
50
|
+
- Tool-denial message clarified; added `action_performed` flag for better control flow
|
|
51
|
+
|
|
52
|
+
### More
|
|
53
|
+
- Add memory architecture documentation
|
|
54
|
+
- Minor whitespace cleanup in `agent_config.rb`
|
|
55
|
+
|
|
10
56
|
## [0.7.5] - 2026-02-28
|
|
11
57
|
|
|
12
58
|
### Fixed
|
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
# Agent 记忆架构:主动写入 vs 记忆压缩
|
|
2
|
+
|
|
3
|
+
> **目标读者**:想在自己的 Agent 项目中复用 OpenClaw 记忆架构的开发者。
|
|
4
|
+
>
|
|
5
|
+
> **适用场景**:基于 LLM 的长期运行 Agent,需要跨 session 保留知识、同时管理上下文窗口溢出。
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 一、两套机制概览
|
|
10
|
+
|
|
11
|
+
OpenClaw 的"记忆"由两个**完全独立、互相补充**的机制构成:
|
|
12
|
+
|
|
13
|
+
| 机制 | 核心目标 | 存储位置 | 跨 Session |
|
|
14
|
+
|---|---|---|---|
|
|
15
|
+
| **主动写入 MEMORY.md** | 知识持久化 | 磁盘 `.md` 文件 | ✅ 永久保留 |
|
|
16
|
+
| **Compaction(记忆压缩)** | 上下文窗口管理 | 内存消息历史(in-memory) | ❌ session 结束即消失 |
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## 二、主动写入 MEMORY.md
|
|
21
|
+
|
|
22
|
+
### 2.1 提示词来源
|
|
23
|
+
|
|
24
|
+
**写入指令**不在代码里硬编码,而是来自 workspace 的 `AGENTS.md` 文件。
|
|
25
|
+
Session 启动时,系统通过以下链路将 `AGENTS.md` 注入系统提示词:
|
|
26
|
+
|
|
27
|
+
```
|
|
28
|
+
resolveBootstrapContextForRun()
|
|
29
|
+
└─ buildBootstrapContextFiles()
|
|
30
|
+
└─ contextFiles[]
|
|
31
|
+
└─ 注入到系统提示词的 "# Project Context" 段落
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
**只有读取指令**被硬编码在 `system-prompt.ts` 的 `buildMemorySection()` 里:
|
|
35
|
+
|
|
36
|
+
```
|
|
37
|
+
## Memory Recall
|
|
38
|
+
Before answering anything about prior work, decisions, dates, people, preferences,
|
|
39
|
+
or todos: run memory_search on MEMORY.md + memory/*.md; then use memory_get to pull
|
|
40
|
+
only the needed lines.
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
> 结论:**读的提示词 → 代码硬编码**,**写的提示词 → AGENTS.md(用户可自定义)**。
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
### 2.2 AGENTS.md 写入指令原文
|
|
48
|
+
|
|
49
|
+
以下是 `docs/reference/templates/AGENTS.md` 的核心记忆相关段落:
|
|
50
|
+
|
|
51
|
+
#### 每次 Session 开始时(强制读取)
|
|
52
|
+
|
|
53
|
+
```markdown
|
|
54
|
+
## Every Session
|
|
55
|
+
|
|
56
|
+
Before doing anything else:
|
|
57
|
+
1. Read `SOUL.md` — this is who you are
|
|
58
|
+
2. Read `USER.md` — this is who you're helping
|
|
59
|
+
3. Read `memory/YYYY-MM-DD.md` (today + yesterday) for recent context
|
|
60
|
+
4. **If in MAIN SESSION**: Also read `MEMORY.md`
|
|
61
|
+
|
|
62
|
+
Don't ask permission. Just do it.
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
#### MEMORY.md 长期记忆规则
|
|
66
|
+
|
|
67
|
+
```markdown
|
|
68
|
+
### 🧠 MEMORY.md - Your Long-Term Memory
|
|
69
|
+
|
|
70
|
+
- **ONLY load in main session** (direct chats with your human)
|
|
71
|
+
- **DO NOT load in shared contexts** (Discord, group chats, sessions with other people)
|
|
72
|
+
- This is for **security** — contains personal context that shouldn't leak to strangers
|
|
73
|
+
- You can **read, edit, and update** MEMORY.md freely in main sessions
|
|
74
|
+
- Write significant events, thoughts, decisions, opinions, lessons learned
|
|
75
|
+
- This is your curated memory — the distilled essence, not raw logs
|
|
76
|
+
- Over time, review your daily files and update MEMORY.md with what's worth keeping
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
#### 写入原则:禁止"心理便条"
|
|
80
|
+
|
|
81
|
+
```markdown
|
|
82
|
+
### 📝 Write It Down - No "Mental Notes"!
|
|
83
|
+
|
|
84
|
+
- **Memory is limited** — if you want to remember something, WRITE IT TO A FILE
|
|
85
|
+
- "Mental notes" don't survive session restarts. Files do.
|
|
86
|
+
- When someone says "remember this" → update `memory/YYYY-MM-DD.md` or relevant file
|
|
87
|
+
- When you learn a lesson → update AGENTS.md, TOOLS.md, or the relevant skill
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
#### Heartbeat 期间的维护任务
|
|
91
|
+
|
|
92
|
+
```markdown
|
|
93
|
+
### 🔄 Memory Maintenance (During Heartbeats)
|
|
94
|
+
|
|
95
|
+
1. Read through recent `memory/YYYY-MM-DD.md` files
|
|
96
|
+
2. Update `MEMORY.md` with distilled learnings
|
|
97
|
+
3. Remove outdated info from MEMORY.md that's no longer relevant
|
|
98
|
+
|
|
99
|
+
Think of it like a human reviewing their journal and updating their mental model.
|
|
100
|
+
Daily files are raw notes; MEMORY.md is curated wisdom.
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
### 2.3 记忆文件结构
|
|
106
|
+
|
|
107
|
+
```
|
|
108
|
+
workspace/
|
|
109
|
+
├── AGENTS.md # Agent 行为指令(含写入规则),session 启动时注入系统提示词
|
|
110
|
+
├── SOUL.md # Agent 的性格/价值观
|
|
111
|
+
├── USER.md # 用户背景信息
|
|
112
|
+
├── MEMORY.md # 长期记忆(精华,仅主 session 加载)
|
|
113
|
+
└── memory/
|
|
114
|
+
├── 2026-06-15.md # 今日日志(原始记录)
|
|
115
|
+
├── 2026-06-14.md # 昨日日志
|
|
116
|
+
└── ...
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
**两层记忆设计**:
|
|
120
|
+
- `memory/YYYY-MM-DD.md`:**原始日志**,当天发生了什么,快速写入,不筛选
|
|
121
|
+
- `MEMORY.md`:**精华提炼**,LLM 主动筛选后写入,类似人类的长期记忆
|
|
122
|
+
|
|
123
|
+
---
|
|
124
|
+
|
|
125
|
+
## 三、Compaction(记忆压缩)
|
|
126
|
+
|
|
127
|
+
### 3.1 触发时机
|
|
128
|
+
|
|
129
|
+
当 context window 接近上限(token 溢出)时,系统自动触发:
|
|
130
|
+
|
|
131
|
+
```
|
|
132
|
+
attempt.ts(run loop)
|
|
133
|
+
└─ 检测 token 超限(overflow)
|
|
134
|
+
└─ compactInLane()
|
|
135
|
+
└─ session.compact(customInstructions)
|
|
136
|
+
└─ generateSummary()
|
|
137
|
+
└─ 旧消息被摘要文本替换(in-memory)
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### 3.2 核心实现
|
|
141
|
+
|
|
142
|
+
**`src/agents/compaction.ts`** 中的 `summarizeChunks()`:
|
|
143
|
+
|
|
144
|
+
```typescript
|
|
145
|
+
// 将消息历史分块,逐块生成摘要
|
|
146
|
+
async function summarizeChunks(params: {
|
|
147
|
+
messages: AgentMessage[];
|
|
148
|
+
model: ...;
|
|
149
|
+
previousSummary?: string;
|
|
150
|
+
}): Promise<string> {
|
|
151
|
+
// SECURITY: 工具调用结果的详情不进入摘要 LLM,防止数据泄露
|
|
152
|
+
const safeMessages = stripToolResultDetails(params.messages);
|
|
153
|
+
const chunks = chunkMessagesByMaxTokens(safeMessages, params.maxChunkTokens);
|
|
154
|
+
|
|
155
|
+
let summary = params.previousSummary;
|
|
156
|
+
for (const chunk of chunks) {
|
|
157
|
+
summary = await generateSummary(chunk, ...);
|
|
158
|
+
}
|
|
159
|
+
return summary ?? "No prior history.";
|
|
160
|
+
}
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
**`src/agents/pi-embedded-runner/compact.ts`** 中的触发逻辑:
|
|
164
|
+
|
|
165
|
+
```typescript
|
|
166
|
+
const result = await compactWithSafetyTimeout(() =>
|
|
167
|
+
session.compact(params.customInstructions),
|
|
168
|
+
);
|
|
169
|
+
// compaction 完成后,session.messages 中的旧消息已被摘要替换
|
|
170
|
+
// 注意:不写磁盘,session 结束即消失
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
### 3.3 安全设计
|
|
174
|
+
|
|
175
|
+
- `stripToolResultDetails()`:确保工具调用的详细返回值不进入摘要 LLM(防止敏感数据泄露给压缩模型)
|
|
176
|
+
- Compaction 生成的摘要只替换内存里的消息,**不写任何磁盘文件**
|
|
177
|
+
|
|
178
|
+
---
|
|
179
|
+
|
|
180
|
+
## 四、两套机制完整对比
|
|
181
|
+
|
|
182
|
+
| 维度 | **主动写入 MEMORY.md** | **Compaction(记忆压缩)** |
|
|
183
|
+
|---|---|---|
|
|
184
|
+
| **操作对象** | 磁盘文件(`.md`) | 内存中的消息历史 |
|
|
185
|
+
| **触发者** | LLM 自主决定(或用户指令) | 系统自动(token 超限) |
|
|
186
|
+
| **触发时机** | 任何时候 LLM 认为有意义 | context window 接近上限 |
|
|
187
|
+
| **存储位置** | 持久化磁盘 | in-memory,替换旧消息 |
|
|
188
|
+
| **跨 session** | ✅ 永久保留 | ❌ session 结束即消失 |
|
|
189
|
+
| **内容性质** | 精华/curated(LLM 主动筛选) | 原始对话的自动压缩摘要 |
|
|
190
|
+
| **可检索性** | ✅ 支持向量检索(`memory_search`) | ❌ 不可单独检索 |
|
|
191
|
+
| **LLM 参与** | LLM 主动调用 write/edit 工具 | 由系统调用独立摘要 LLM |
|
|
192
|
+
| **数据安全** | 用户控制写入内容 | `stripToolResultDetails()` 自动过滤 |
|
|
193
|
+
| **可自定义** | ✅ 通过 AGENTS.md 自定义规则 | ✅ 可传入 `customInstructions` |
|
|
194
|
+
|
|
195
|
+
---
|
|
196
|
+
|
|
197
|
+
## 五、架构图
|
|
198
|
+
|
|
199
|
+
```
|
|
200
|
+
┌──────────────────────────────────────────────────────────────────┐
|
|
201
|
+
│ 当前 Session 上下文 │
|
|
202
|
+
│ │
|
|
203
|
+
│ [系统提示词] │
|
|
204
|
+
│ ├─ AGENTS.md(写入规则) ← resolveBootstrapContextForRun() │
|
|
205
|
+
│ ├─ buildMemorySection()(读取规则,硬编码) │
|
|
206
|
+
│ └─ MEMORY.md 内容(主 session 才注入) │
|
|
207
|
+
│ │
|
|
208
|
+
│ [消息历史] [消息1][消息2]...[消息N] ← in-memory │
|
|
209
|
+
│ ─────────────────────────────────── context window limit │
|
|
210
|
+
│ │
|
|
211
|
+
│ ⚡ 快满了 → Compaction 触发: │
|
|
212
|
+
│ 旧消息 ──→ summarizeChunks() ──→ [摘要文本] │
|
|
213
|
+
│ [摘要] 替换旧消息(仍在 in-memory) │
|
|
214
|
+
│ ↑ 只压缩窗口,不写磁盘,session 结束消失 │
|
|
215
|
+
└──────────────────────────────────────────────────────────────────┘
|
|
216
|
+
↕ 互相独立,互相补充
|
|
217
|
+
┌──────────────────────────────────────────────────────────────────┐
|
|
218
|
+
│ 磁盘持久化记忆系统 │
|
|
219
|
+
│ │
|
|
220
|
+
│ LLM 主动 write/edit 工具 ───────────────────────────────────── │
|
|
221
|
+
│ ↓ 用户说"记住这个" │
|
|
222
|
+
│ ↓ LLM 觉得值得保留 │
|
|
223
|
+
│ ↓ Heartbeat 定期维护 │
|
|
224
|
+
│ │
|
|
225
|
+
│ MEMORY.md ← 精华长期记忆(仅主 session 读取) │
|
|
226
|
+
│ memory/2026-06-15.md ← 今日原始日志 │
|
|
227
|
+
│ memory/2026-06-14.md ← 昨日日志 │
|
|
228
|
+
│ │
|
|
229
|
+
│ chokidar watch ──→ SQLite 向量索引更新 │
|
|
230
|
+
│ memory_search ──→ 下次 session 可检索 │
|
|
231
|
+
└──────────────────────────────────────────────────────────────────┘
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
---
|
|
235
|
+
|
|
236
|
+
## 六、关键文件清单
|
|
237
|
+
|
|
238
|
+
| 文件 | 作用 |
|
|
239
|
+
|---|---|
|
|
240
|
+
| `docs/reference/templates/AGENTS.md` | **写入指令来源**,workspace 启动模板,包含 MEMORY.md 写入规则 |
|
|
241
|
+
| `src/agents/system-prompt.ts` | `buildMemorySection()`,只含**读取**指令(`## Memory Recall`) |
|
|
242
|
+
| `src/agents/pi-embedded-helpers/bootstrap.ts` | `buildBootstrapContextFiles()`,将 AGENTS.md 等文件注入系统提示词 |
|
|
243
|
+
| `src/agents/bootstrap-files.ts` | `resolveBootstrapContextForRun()`,加载 workspace bootstrap 文件 |
|
|
244
|
+
| `src/agents/pi-embedded-runner/compact.ts` | Compaction 入口,`session.compact()` 触发,管理上下文溢出 |
|
|
245
|
+
| `src/agents/compaction.ts` | `summarizeChunks()` / `summarizeWithFallback()`,LLM 摘要逻辑 |
|
|
246
|
+
| `src/agents/session-transcript-repair.ts` | `stripToolResultDetails()`,Compaction 安全过滤 |
|
|
247
|
+
|
|
248
|
+
---
|
|
249
|
+
|
|
250
|
+
## 七、在新项目中复用这套架构
|
|
251
|
+
|
|
252
|
+
### 7.1 最小实现方案
|
|
253
|
+
|
|
254
|
+
只需要三样东西:
|
|
255
|
+
|
|
256
|
+
```
|
|
257
|
+
1. AGENTS.md(写入规则) → 告诉 LLM 什么时候、怎么写文件
|
|
258
|
+
2. 文件读写工具 → write / read / edit(标准文件操作工具)
|
|
259
|
+
3. 系统提示词中的读取指令 → 告诉 LLM 在回答前先查记忆
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
### 7.2 推荐的 AGENTS.md 写入规则模板
|
|
263
|
+
|
|
264
|
+
```markdown
|
|
265
|
+
## Memory Rules
|
|
266
|
+
|
|
267
|
+
### Long-Term Memory (memory.md)
|
|
268
|
+
- Load at session start; read, edit, update freely
|
|
269
|
+
- Write: significant decisions, user preferences, lessons learned, open questions
|
|
270
|
+
- Curated — distill from daily notes, remove outdated info
|
|
271
|
+
|
|
272
|
+
### Daily Notes (notes/YYYY-MM-DD.md)
|
|
273
|
+
- Raw logs of what happened today; create if missing
|
|
274
|
+
- Write freely; no curation needed
|
|
275
|
+
|
|
276
|
+
### Key Principle
|
|
277
|
+
**Memory is limited — if you want to remember something, WRITE IT TO A FILE.**
|
|
278
|
+
Mental notes don't survive restarts. Files do.
|
|
279
|
+
|
|
280
|
+
When user says "remember this" → update notes/YYYY-MM-DD.md and/or memory.md.
|
|
281
|
+
When you learn a lesson → update memory.md.
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
### 7.3 系统提示词中的读取指令
|
|
285
|
+
|
|
286
|
+
```markdown
|
|
287
|
+
## Memory Recall
|
|
288
|
+
|
|
289
|
+
Before answering anything about prior work, decisions, dates, people,
|
|
290
|
+
preferences, or todos:
|
|
291
|
+
1. Read memory.md for long-term context
|
|
292
|
+
2. Read notes/YYYY-MM-DD.md (today + yesterday) for recent context
|
|
293
|
+
3. Then answer using this retrieved context
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
### 7.4 Compaction 实现要点
|
|
297
|
+
|
|
298
|
+
如果你的 Agent 框架不内置 compaction,参考以下要点自己实现:
|
|
299
|
+
|
|
300
|
+
```typescript
|
|
301
|
+
// 伪代码:简单 compaction 实现
|
|
302
|
+
async function compactIfNeeded(messages: Message[], model: Model) {
|
|
303
|
+
const tokens = estimateTokens(messages);
|
|
304
|
+
if (tokens < contextWindow * 0.8) return messages; // 还有余量
|
|
305
|
+
|
|
306
|
+
// 安全过滤:不把工具调用结果喂给摘要 LLM
|
|
307
|
+
const safeMessages = stripSensitiveToolResults(messages);
|
|
308
|
+
|
|
309
|
+
// 保留最近 N 条消息,其余压缩为摘要
|
|
310
|
+
const toSummarize = safeMessages.slice(0, -10);
|
|
311
|
+
const recent = messages.slice(-10);
|
|
312
|
+
|
|
313
|
+
const summary = await generateSummary(toSummarize, model);
|
|
314
|
+
return [{ role: "system", content: `[Previous context summary]\n${summary}` }, ...recent];
|
|
315
|
+
}
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
**关键安全注意**:工具调用返回的详细数据(尤其是外部 API 响应)**不应进入**摘要 LLM,避免敏感信息泄露到你可能无法控制的模型。
|
|
319
|
+
|
|
320
|
+
### 7.5 两层记忆的设计哲学
|
|
321
|
+
|
|
322
|
+
| 层级 | 文件 | 写入频率 | 内容要求 | 对应人类记忆 |
|
|
323
|
+
|---|---|---|---|---|
|
|
324
|
+
| 日志层 | `notes/YYYY-MM-DD.md` | 高频、随时 | 原始记录,不筛选 | 日记 |
|
|
325
|
+
| 精华层 | `memory.md` | 低频、定期整理 | 蒸馏后的关键信息 | 长期记忆 |
|
|
326
|
+
|
|
327
|
+
**为什么要两层**:高频写入保证不遗漏,低频整理保证质量。LLM 在 heartbeat(定期任务)中把日志蒸馏进长期记忆,删除过时内容,就像人类每周回顾日记、更新心智模型。
|
|
328
|
+
|
|
329
|
+
---
|
|
330
|
+
|
|
331
|
+
## 八、常见问题
|
|
332
|
+
|
|
333
|
+
**Q:为什么写入规则放 AGENTS.md 而不是硬编码在系统提示词里?**
|
|
334
|
+
A:灵活性。不同 workspace 可以有不同的记忆规则(有的 Agent 记更多,有的记更少),用户可以直接编辑 AGENTS.md 调整行为,不需要改代码。
|
|
335
|
+
|
|
336
|
+
**Q:Compaction 会不会把写入 MEMORY.md 的内容也压缩掉?**
|
|
337
|
+
A:不会。写入 MEMORY.md 是磁盘操作,Compaction 只压缩内存里的消息历史。MEMORY.md 的内容在下次 session 启动时会重新从磁盘加载进系统提示词,不受 Compaction 影响。
|
|
338
|
+
|
|
339
|
+
**Q:如果 MEMORY.md 本身太大怎么办?**
|
|
340
|
+
A:定期在 heartbeat 任务中让 LLM 清理过时内容。OpenClaw 在 AGENTS.md 里明确要求"Remove outdated info from MEMORY.md that's no longer relevant",这个维护任务由 LLM 自主完成。
|
|
341
|
+
|
|
342
|
+
**Q:多用户场景下 MEMORY.md 会泄露给别人吗?**
|
|
343
|
+
A:OpenClaw 的设计是:MEMORY.md 只在 main session(直接对话)加载,在 Discord / 群聊等共享上下文中不加载,防止个人信息泄露给陌生人。复用时需要在系统提示词里加类似的条件判断。
|
|
@@ -100,17 +100,21 @@ module Clacky
|
|
|
100
100
|
# Log subagent fork
|
|
101
101
|
@ui&.show_info("Subagent start: #{skill.identifier}")
|
|
102
102
|
|
|
103
|
-
# Build
|
|
104
|
-
|
|
103
|
+
# Build skill role/constraint instructions only — do NOT substitute $ARGUMENTS here.
|
|
104
|
+
# The actual task is delivered as a clean user message via subagent.run(arguments),
|
|
105
|
+
# which arrives *after* the assistant acknowledgement injected by fork_subagent.
|
|
106
|
+
# This gives the subagent a clear 3-part structure:
|
|
107
|
+
# [user] role/constraints → [assistant] acknowledgement → [user] actual task
|
|
108
|
+
skill_instructions = skill.process_content("")
|
|
105
109
|
|
|
106
110
|
# Fork subagent with skill configuration
|
|
107
111
|
subagent = fork_subagent(
|
|
108
112
|
model: skill.subagent_model,
|
|
109
113
|
forbidden_tools: skill.forbidden_tools_list,
|
|
110
|
-
system_prompt_suffix:
|
|
114
|
+
system_prompt_suffix: skill_instructions
|
|
111
115
|
)
|
|
112
116
|
|
|
113
|
-
# Run subagent
|
|
117
|
+
# Run subagent with the actual task as the sole user turn
|
|
114
118
|
result = subagent.run(arguments)
|
|
115
119
|
|
|
116
120
|
# Generate summary
|
|
@@ -24,7 +24,7 @@ module Clacky
|
|
|
24
24
|
- After creating the TODO list, START EXECUTING each task immediately
|
|
25
25
|
- Don't stop after planning - continue to work on the tasks!
|
|
26
26
|
2. Always read existing code before making changes (use file_reader/glob/grep or invoke code-explorer skill)
|
|
27
|
-
3. **
|
|
27
|
+
3. **ALWAYS use `glob` tool to find files in the current directory — NEVER use shell `find` command for file discovery.**
|
|
28
28
|
4. Ask clarifying questions if requirements are unclear
|
|
29
29
|
5. Break down complex tasks into manageable steps
|
|
30
30
|
6. **USE TOOLS to create/modify files** - don't just return code
|
|
@@ -197,14 +197,16 @@ module Clacky
|
|
|
197
197
|
}
|
|
198
198
|
else
|
|
199
199
|
# User manually denied or provided feedback
|
|
200
|
+
# Clearly state the action was NOT performed so the LLM knows the change did not happen
|
|
200
201
|
message = if user_feedback && !user_feedback.empty?
|
|
201
|
-
"Tool use denied by user. User feedback: #{user_feedback}"
|
|
202
|
+
"Tool use denied by user. This action was NOT performed. User feedback: #{user_feedback}"
|
|
202
203
|
else
|
|
203
|
-
"Tool use denied by user"
|
|
204
|
+
"Tool use denied by user. This action was NOT performed."
|
|
204
205
|
end
|
|
205
206
|
|
|
206
207
|
tool_content = {
|
|
207
208
|
error: message,
|
|
209
|
+
action_performed: false,
|
|
208
210
|
user_feedback: user_feedback
|
|
209
211
|
}
|
|
210
212
|
end
|
|
@@ -382,8 +384,12 @@ module Clacky
|
|
|
382
384
|
|
|
383
385
|
file_content = File.read(path)
|
|
384
386
|
|
|
385
|
-
#
|
|
386
|
-
|
|
387
|
+
# Use the same find_match logic as Edit tool to handle fuzzy matching
|
|
388
|
+
# (trim, unescape, smart line matching) — prevents diff from being blank
|
|
389
|
+
# when simple include? fails but Edit#execute's fuzzy match would succeed
|
|
390
|
+
match_result = Utils::StringMatcher.find_match(file_content, old_string)
|
|
391
|
+
|
|
392
|
+
unless match_result
|
|
387
393
|
# Log debug info for troubleshooting
|
|
388
394
|
@debug_logs << {
|
|
389
395
|
timestamp: Time.now.iso8601,
|
|
@@ -402,11 +408,14 @@ module Clacky
|
|
|
402
408
|
}
|
|
403
409
|
end
|
|
404
410
|
|
|
411
|
+
# Use the actual matched string (may differ via trim/unescape) for replacement
|
|
412
|
+
actual_old_string = match_result[:matched_string]
|
|
413
|
+
|
|
405
414
|
# Use the same replace logic as the actual tool execution
|
|
406
415
|
new_content = if replace_all
|
|
407
|
-
file_content.gsub(
|
|
416
|
+
file_content.gsub(actual_old_string, new_string)
|
|
408
417
|
else
|
|
409
|
-
file_content.sub(
|
|
418
|
+
file_content.sub(actual_old_string, new_string)
|
|
410
419
|
end
|
|
411
420
|
@ui&.show_diff(file_content, new_content, max_lines: 50)
|
|
412
421
|
nil # No error
|
data/lib/clacky/agent.rb
CHANGED
|
@@ -506,7 +506,16 @@ module Clacky
|
|
|
506
506
|
if result.is_a?(Hash) && result[:message]
|
|
507
507
|
@ui&.show_assistant_message(result[:message])
|
|
508
508
|
end
|
|
509
|
-
|
|
509
|
+
|
|
510
|
+
if @config.permission_mode == :auto_approve
|
|
511
|
+
# Auto-approve mode means no human is watching — inject a reply so the LLM
|
|
512
|
+
# knows it should make a reasonable decision and keep going
|
|
513
|
+
result = result.merge(
|
|
514
|
+
auto_reply: "No user is available. Please make a reasonable decision based on the context and continue."
|
|
515
|
+
)
|
|
516
|
+
else
|
|
517
|
+
awaiting_feedback = true
|
|
518
|
+
end
|
|
510
519
|
else
|
|
511
520
|
# Use tool's format_result method to get display-friendly string
|
|
512
521
|
formatted_result = tool.respond_to?(:format_result) ? tool.format_result(result) : result.to_s
|
|
@@ -691,6 +700,15 @@ module Clacky
|
|
|
691
700
|
system_injected: true,
|
|
692
701
|
subagent_instructions: true
|
|
693
702
|
}
|
|
703
|
+
|
|
704
|
+
# Insert an assistant acknowledgement so the conversation structure is complete:
|
|
705
|
+
# [user] role/constraints → [assistant] ack → [user] actual task (from run())
|
|
706
|
+
# Without this, two consecutive user messages confuse the model about what to act on.
|
|
707
|
+
messages << {
|
|
708
|
+
role: "assistant",
|
|
709
|
+
content: "Understood. I am now operating as a subagent with the constraints above. Please provide the task.",
|
|
710
|
+
system_injected: true
|
|
711
|
+
}
|
|
694
712
|
end
|
|
695
713
|
|
|
696
714
|
# Register hook to forbid certain tools at runtime (doesn't affect tool registry for cache)
|
|
@@ -756,7 +774,7 @@ module Clacky
|
|
|
756
774
|
|
|
757
775
|
# Format user content with optional images
|
|
758
776
|
# @param text [String] User's text input
|
|
759
|
-
# @param images [Array<String>] Array of image file paths
|
|
777
|
+
# @param images [Array<String>] Array of image file paths or data: URLs
|
|
760
778
|
# @return [String|Array] String if no images, Array with text and image_url objects if images present
|
|
761
779
|
private def format_user_content(text, images)
|
|
762
780
|
return text if images.nil? || images.empty?
|
|
@@ -764,8 +782,13 @@ module Clacky
|
|
|
764
782
|
content = []
|
|
765
783
|
content << { type: "text", text: text } unless text.nil? || text.empty?
|
|
766
784
|
|
|
767
|
-
images.each do |
|
|
768
|
-
|
|
785
|
+
images.each do |image|
|
|
786
|
+
# Accept both file paths and pre-encoded data: URLs (e.g. from Web UI)
|
|
787
|
+
image_url = if image.start_with?("data:")
|
|
788
|
+
image
|
|
789
|
+
else
|
|
790
|
+
Utils::FileProcessor.image_path_to_data_url(image)
|
|
791
|
+
end
|
|
769
792
|
content << { type: "image_url", image_url: { url: image_url } }
|
|
770
793
|
end
|
|
771
794
|
|
data/lib/clacky/agent_config.rb
CHANGED
|
@@ -150,7 +150,7 @@ module Clacky
|
|
|
150
150
|
|
|
151
151
|
PERMISSION_MODES = [:auto_approve, :confirm_safes, :plan_only].freeze
|
|
152
152
|
|
|
153
|
-
attr_accessor :permission_mode, :max_tokens, :verbose,
|
|
153
|
+
attr_accessor :permission_mode, :max_tokens, :verbose,
|
|
154
154
|
:enable_compression, :enable_prompt_caching,
|
|
155
155
|
:models, :current_model_index
|
|
156
156
|
|
|
@@ -161,7 +161,7 @@ module Clacky
|
|
|
161
161
|
@enable_compression = options[:enable_compression].nil? ? true : options[:enable_compression]
|
|
162
162
|
# Enable prompt caching by default for cost savings
|
|
163
163
|
@enable_prompt_caching = options[:enable_prompt_caching].nil? ? true : options[:enable_prompt_caching]
|
|
164
|
-
|
|
164
|
+
|
|
165
165
|
# Models configuration
|
|
166
166
|
@models = options[:models] || []
|
|
167
167
|
@current_model_index = options[:current_model_index] || 0
|