dsh-soul-forging 0.1.6 → 0.1.8
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.
- package/README.md +61 -0
- package/bin/init-home +71 -6
- package/lib/plugin.js +35 -7
- package/memory/BOOTSTRAP.md +4 -2
- package/package.json +1 -1
- package/scripts/smoke.sh +28 -5
- package/soul.py +78 -20
package/README.md
CHANGED
|
@@ -79,6 +79,36 @@ cd dsh-soul-forging && bash install.sh
|
|
|
79
79
|
|
|
80
80
|
两种都**不需要服务器**。想换记忆位置:设 `MEMORY_DIR`,或插件配置里改 `memoryDir`。
|
|
81
81
|
|
|
82
|
+
### 装的时候可能遇到的两个坑
|
|
83
|
+
|
|
84
|
+
**① 全局安装要 sudo**
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
sudo npm i -g dsh-soul-forging
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
不给 sudo 会报 `EACCES: permission denied, mkdir '/usr/local/lib/node_modules'`。
|
|
91
|
+
不想用 sudo 就换用户级 prefix:`npm config set prefix ~/.npm-global`。
|
|
92
|
+
|
|
93
|
+
**② 国内镜像源会滞后**
|
|
94
|
+
|
|
95
|
+
如果配了淘宝/腾讯云之类的镜像,新版本可能要几小时才同步,装的时候报
|
|
96
|
+
`No matching version found for xxx@0.y.z`。这时候指定官方源:
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
sudo npm i -g dsh-soul-forging --registry https://registry.npmjs.org
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### 数据放在哪
|
|
103
|
+
|
|
104
|
+
**代码和数据是分开的**:
|
|
105
|
+
|
|
106
|
+
- 代码 → `node_modules/dsh-soul-forging/`(升级时会整个换掉)
|
|
107
|
+
- 数据 → `~/.soul-forging/{memory,brain}`(你的记忆,升级不碰)
|
|
108
|
+
|
|
109
|
+
这也是为什么不能用 `sudo` 跑 `soul` 系列命令——`sudo` 会把 `HOME` 变成 `/root`,
|
|
110
|
+
它就会去找 `/root/.soul-forging`,看起来像"记忆丢了"。**日常用普通用户跑。**
|
|
111
|
+
|
|
82
112
|
---
|
|
83
113
|
|
|
84
114
|
## 用
|
|
@@ -117,6 +147,37 @@ soul-sync # 补摘要 → 瘦身 → 建索引 → 重
|
|
|
117
147
|
|
|
118
148
|
---
|
|
119
149
|
|
|
150
|
+
## 三种人,三条路
|
|
151
|
+
|
|
152
|
+
不是每个人都要一个"有性格的 AI"。所以装的时候可以选:
|
|
153
|
+
|
|
154
|
+
```bash
|
|
155
|
+
soul-init # 全套:记忆 + 性格模板(想长性格的人)
|
|
156
|
+
soul-init --memory-only # 只要记忆,不要性格
|
|
157
|
+
soul-init --external # 性格归你自己的 agent,我们只管记忆
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
| 模式 | 注入什么 | 适合谁 |
|
|
161
|
+
|---|---|---|
|
|
162
|
+
| `full` | SOUL + MEMORY + index + NOW | 想从零长出一个性格 |
|
|
163
|
+
| `memory-only` | MEMORY + index + NOW | 只想让它记得住,不想动它的人设 |
|
|
164
|
+
| `external` | index + NOW | 你的 agent 已经有自己的 prompt/人设了 |
|
|
165
|
+
|
|
166
|
+
对应地,`soul-boot` 也能选:
|
|
167
|
+
|
|
168
|
+
```bash
|
|
169
|
+
soul-boot --mode memory-only
|
|
170
|
+
soul-boot --with index.md,NOW.md # 更细:自己挑文件
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
**空模板永远不会被注入。** 没填的 SOUL 里全是「(待填)」,塞进上下文比不塞更糟——
|
|
174
|
+
模型会一本正经地介绍"我的名字是(待填)"。所以只要某页的占位符超过 3 处,就自动跳过。
|
|
175
|
+
`--keep-empty` 可以强制保留(调试用)。
|
|
176
|
+
|
|
177
|
+
铸魂清单也有 `status: skip`:用户明确说了不要性格,就别每次开局问他"你叫什么名字"。
|
|
178
|
+
|
|
179
|
+
---
|
|
180
|
+
|
|
120
181
|
## 铸魂
|
|
121
182
|
|
|
122
183
|
装完的第一件事不是干活,是**先把"我是谁"聊清楚**。
|
package/bin/init-home
CHANGED
|
@@ -1,11 +1,25 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
# 把空白记忆库铺到用户目录(装成插件时用这个)
|
|
3
|
+
#
|
|
4
|
+
# soul-init 全套:记忆 + 性格模板(想长性格的人)
|
|
5
|
+
# soul-init --memory-only 只要记忆,不要性格:不生成 SOUL.md,铸魂清单标 skip
|
|
6
|
+
# soul-init --external 性格归你自己的 agent:连 BOOTSTRAP 都不铺
|
|
7
|
+
#
|
|
3
8
|
# 默认 $HOME/.soul-forging/memory,可用 MEMORY_DIR 覆盖
|
|
4
9
|
set -uo pipefail
|
|
5
10
|
_SELF="$(readlink -f "${BASH_SOURCE[0]}" 2>/dev/null || echo "${BASH_SOURCE[0]}")"
|
|
6
11
|
MEMKIT_ROOT="$(cd "$(dirname "$_SELF")/.." && pwd)"
|
|
7
12
|
TARGET="${MEMORY_DIR:-$HOME/.soul-forging/memory}"
|
|
8
13
|
|
|
14
|
+
MODE=full
|
|
15
|
+
case "${1:-}" in
|
|
16
|
+
--memory-only) MODE=memory-only ;;
|
|
17
|
+
--external) MODE=external ;;
|
|
18
|
+
"") ;;
|
|
19
|
+
-h|--help) sed -n '2,10p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;;
|
|
20
|
+
*) echo "不认识的参数:$1"; exit 1 ;;
|
|
21
|
+
esac
|
|
22
|
+
|
|
9
23
|
if [ -d "$TARGET" ] && [ -n "$(ls -A "$TARGET" 2>/dev/null)" ]; then
|
|
10
24
|
echo "目标已存在且有内容:$TARGET"
|
|
11
25
|
echo "不覆盖。要重来先自己备份并删掉它。"
|
|
@@ -16,10 +30,61 @@ mkdir -p "$TARGET"
|
|
|
16
30
|
cp -a "$MEMKIT_ROOT/memory/." "$TARGET/"
|
|
17
31
|
# 数据目录也建出来(索引和服务写这里,别写进包目录)
|
|
18
32
|
mkdir -p "${BRAIN_DIR:-$(dirname "$TARGET")/brain}"
|
|
19
|
-
|
|
33
|
+
|
|
34
|
+
if [ "$MODE" != "full" ]; then
|
|
35
|
+
# 不要性格:SOUL 是个空模板,留着只会让模型看到"我的名字是(待填)"
|
|
36
|
+
rm -f "$TARGET/SOUL.md"
|
|
37
|
+
cat > "$TARGET/SOUL-NOTE.md" <<'NOTE'
|
|
38
|
+
# 关于 SOUL.md
|
|
39
|
+
|
|
40
|
+
这个记忆库是按"不要性格"装的:**没有 SOUL.md**,注入时也不会带性格那一块。
|
|
41
|
+
|
|
42
|
+
- 想只要记忆、不要性格 → 保持现状,`soul-boot` 默认用 `memory-only` 模式
|
|
43
|
+
- 以后想长性格了 → 把 `BOOTSTRAP.md` 的状态改回 `pending`,跑 `soul-init --force-soul` 或手写一份 `SOUL.md`
|
|
44
|
+
- 记忆本身照常工作,跟性格无关
|
|
45
|
+
NOTE
|
|
46
|
+
fi
|
|
47
|
+
|
|
48
|
+
if [ "$MODE" = "external" ]; then
|
|
49
|
+
rm -f "$TARGET/BOOTSTRAP.md"
|
|
50
|
+
echo "--external" > "$TARGET/.mode"
|
|
51
|
+
else
|
|
52
|
+
echo "$MODE" > "$TARGET/.mode"
|
|
53
|
+
fi
|
|
54
|
+
|
|
55
|
+
# 把铸魂清单标成 skip(memory-only 时),别每次开局催用户
|
|
56
|
+
if [ "$MODE" = "memory-only" ] && [ -f "$TARGET/BOOTSTRAP.md" ]; then
|
|
57
|
+
# 行尾可能有注释,所以匹配整行而不是 ^status: pending$
|
|
58
|
+
python3 - "$TARGET/BOOTSTRAP.md" <<'PY'
|
|
59
|
+
import re, sys
|
|
60
|
+
p = sys.argv[1]
|
|
61
|
+
try:
|
|
62
|
+
s = open(p, encoding="utf-8").read()
|
|
63
|
+
except OSError:
|
|
64
|
+
sys.exit(0)
|
|
65
|
+
s = re.sub(r"^status:\s*\w+.*$", "status: skip", s, count=1, flags=re.M)
|
|
66
|
+
open(p, "w", encoding="utf-8").write(s)
|
|
67
|
+
PY
|
|
68
|
+
fi
|
|
69
|
+
|
|
70
|
+
echo "✓ 空白记忆库已铺到:$TARGET(模式:$MODE)"
|
|
20
71
|
echo
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
echo "
|
|
24
|
-
echo "
|
|
25
|
-
echo "
|
|
72
|
+
case "$MODE" in
|
|
73
|
+
full)
|
|
74
|
+
echo "接下来:"
|
|
75
|
+
echo " 1. 填 $TARGET/SOUL.md"
|
|
76
|
+
echo " 2. 按 $TARGET/BOOTSTRAP.md 走一遍铸魂"
|
|
77
|
+
;;
|
|
78
|
+
memory-only)
|
|
79
|
+
echo "接下来:直接开始记东西就行,不用管性格。"
|
|
80
|
+
echo " · 注入会自动跳过空的 SOUL"
|
|
81
|
+
echo " · 想改主意:把 $TARGET/BOOTSTRAP.md 的 status 改回 pending"
|
|
82
|
+
;;
|
|
83
|
+
external)
|
|
84
|
+
echo "接下来:性格由你自己的 agent prompt 负责,这里只管记忆。"
|
|
85
|
+
echo " · 注入只有 index.md + NOW.md"
|
|
86
|
+
;;
|
|
87
|
+
esac
|
|
88
|
+
echo " · 建索引:python3 $MEMKIT_ROOT/brain/index.py"
|
|
89
|
+
echo " · 起服务:bash $MEMKIT_ROOT/bin/serve"
|
|
90
|
+
echo " · 看注入:soul-boot --list"
|
package/lib/plugin.js
CHANGED
|
@@ -23,7 +23,25 @@ const HERE = dirname(fileURLToPath(import.meta.url))
|
|
|
23
23
|
const KIT_ROOT = resolve(HERE, '..')
|
|
24
24
|
|
|
25
25
|
// 注入顺序:先是谁,再是什么规矩,再是目录,最后是此刻。
|
|
26
|
-
const
|
|
26
|
+
const ALL_PARTS = ['SOUL.md', 'MEMORY.md', 'index.md', 'NOW.md']
|
|
27
|
+
|
|
28
|
+
// 三种模式 —— 跟 soul.py 里的 MODES 保持一致
|
|
29
|
+
const MODES = {
|
|
30
|
+
full: ALL_PARTS,
|
|
31
|
+
// 不要性格:空 SOUL 塞进去比不塞更糟(模型会看到"我的名字是(待填)")
|
|
32
|
+
'memory-only': ['MEMORY.md', 'index.md', 'NOW.md'],
|
|
33
|
+
external: ['index.md', 'NOW.md'],
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// 空模板检测:占位符够多就跳过,别把模型带偏
|
|
37
|
+
const PLACEHOLDER_MARKS = ['(待填', '(待填', 'TODO', '(示例', '(还没有']
|
|
38
|
+
const PLACEHOLDER_MIN = 3
|
|
39
|
+
|
|
40
|
+
function isPlaceholder(body) {
|
|
41
|
+
let n = 0
|
|
42
|
+
for (const m of PLACEHOLDER_MARKS) n += body.split(m).length - 1
|
|
43
|
+
return n >= PLACEHOLDER_MIN
|
|
44
|
+
}
|
|
27
45
|
|
|
28
46
|
function defaultMemoryDir() {
|
|
29
47
|
// 不放在 node_modules 里——升级插件会把那里整个换掉。
|
|
@@ -41,13 +59,19 @@ function readIf(p) {
|
|
|
41
59
|
/**
|
|
42
60
|
* 拼注入块。budget 是字数上限,超了从末尾截(先保 SOUL)。
|
|
43
61
|
* 注意:这里是**字符数**,不是字节数。
|
|
62
|
+
*
|
|
63
|
+
* files 指定要哪些文件(优先级高于 mode)
|
|
64
|
+
* mode full / memory-only / external
|
|
65
|
+
* skipEmpty 自动跳过仍是空模板的页(默认开)
|
|
44
66
|
*/
|
|
45
|
-
export function bootBlock(memoryDir, budget = 20000) {
|
|
67
|
+
export function bootBlock(memoryDir, budget = 20000, { files = null, mode = 'full', skipEmpty = true } = {}) {
|
|
68
|
+
const parts = files && files.length ? files : MODES[mode] || ALL_PARTS
|
|
46
69
|
const chunks = []
|
|
47
70
|
let total = 0
|
|
48
|
-
for (const name of
|
|
71
|
+
for (const name of parts) {
|
|
49
72
|
const body = readIf(join(memoryDir, name))
|
|
50
73
|
if (!body) continue
|
|
74
|
+
if (skipEmpty && isPlaceholder(body)) continue
|
|
51
75
|
let block = `<!-- ${name} -->\n${body}`
|
|
52
76
|
if (total + block.length > budget) {
|
|
53
77
|
const left = budget - total
|
|
@@ -59,7 +83,7 @@ export function bootBlock(memoryDir, budget = 20000) {
|
|
|
59
83
|
chunks.push(block)
|
|
60
84
|
total += block.length
|
|
61
85
|
}
|
|
62
|
-
if (!chunks.length) return ''
|
|
86
|
+
if (!chunks.length) return '(记忆库还是空的,暂时没有可注入的内容。)'
|
|
63
87
|
return (
|
|
64
88
|
'下面是你的长期记忆。你确实记得这些事,像想起往事一样自然地用,' +
|
|
65
89
|
'不要说「根据记忆库」「据记录」这类话。\n\n' + chunks.join('\n\n')
|
|
@@ -106,6 +130,10 @@ const SKILL = {
|
|
|
106
130
|
export function apply(ctx, config = {}) {
|
|
107
131
|
const memoryDir = config.memoryDir || defaultMemoryDir()
|
|
108
132
|
const budget = Number(config.bootMaxChars || 20000)
|
|
133
|
+
// 模式:full / memory-only / external;bootFiles 可直接指定文件名单
|
|
134
|
+
const mode = config.mode || 'full'
|
|
135
|
+
const files = Array.isArray(config.bootFiles) && config.bootFiles.length ? config.bootFiles : null
|
|
136
|
+
const skipEmpty = config.skipEmptyTemplates !== false
|
|
109
137
|
|
|
110
138
|
const disposers = []
|
|
111
139
|
|
|
@@ -115,7 +143,7 @@ export function apply(ctx, config = {}) {
|
|
|
115
143
|
ctx.systemPrompt.context({
|
|
116
144
|
name: 'soul-boot',
|
|
117
145
|
order: -200,
|
|
118
|
-
text: () => bootBlock(memoryDir, budget),
|
|
146
|
+
text: () => bootBlock(memoryDir, budget, { files, mode, skipEmpty }),
|
|
119
147
|
}),
|
|
120
148
|
)
|
|
121
149
|
}
|
|
@@ -135,7 +163,7 @@ export function apply(ctx, config = {}) {
|
|
|
135
163
|
}
|
|
136
164
|
}
|
|
137
165
|
|
|
138
|
-
ctx.logger?.info?.(`[soul-forging] 记忆目录:${memoryDir}
|
|
166
|
+
ctx.logger?.info?.(`[soul-forging] 记忆目录:${memoryDir}(模式 ${mode} · 上限 ${budget} 字)`)
|
|
139
167
|
|
|
140
168
|
return () => {
|
|
141
169
|
for (const d of disposers) {
|
|
@@ -152,4 +180,4 @@ apply.inject = inject
|
|
|
152
180
|
export default apply
|
|
153
181
|
|
|
154
182
|
// 给测试用:不装插件也能直接看注入块长什么样
|
|
155
|
-
export {
|
|
183
|
+
export { ALL_PARTS, MODES, KIT_ROOT }
|
package/memory/BOOTSTRAP.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
-
status: pending
|
|
2
|
+
status: pending # pending 还没定 / complete 定完了 / skip 明确不要性格
|
|
3
3
|
---
|
|
4
4
|
|
|
5
5
|
# BOOTSTRAP — 铸魂
|
|
@@ -37,7 +37,9 @@ status: pending
|
|
|
37
37
|
|
|
38
38
|
## 规则
|
|
39
39
|
|
|
40
|
-
|
|
40
|
+
0. **`status: skip` 时整份清单作废**——用户明确说了不要性格,别再问他"你叫什么名字"。
|
|
41
|
+
什么时候想动了,把 status 改回 `pending` 就重新生效。
|
|
42
|
+
1. `status` 未变成 `complete`(且不是 `skip`)时,每次会话开始的头一件事是**继续这个对话**,不埋头做别的。
|
|
41
43
|
主人明确说要先干别的,就照做,但之后要回来。
|
|
42
44
|
2. 一次推进一项。**不要一口气问完**——那是问卷,不是聊天。
|
|
43
45
|
3. 写回文件之后才勾选,顺序不能反。
|
package/package.json
CHANGED
package/scripts/smoke.sh
CHANGED
|
@@ -90,15 +90,38 @@ run "soul-debug" "$BIN/soul-debug"
|
|
|
90
90
|
echo "== 4. 包目录必须干净(跑完所有命令之后查)=="
|
|
91
91
|
DIRTY=$(check_pkg_clean) && ok "包目录没被写脏" || bad "包目录被污染:$DIRTY"
|
|
92
92
|
|
|
93
|
-
echo "== 5.
|
|
93
|
+
echo "== 5. 三种模式(不想改性格的人)=="
|
|
94
|
+
# memory-only:不该生成 SOUL,注入里不该出现性格
|
|
95
|
+
export MEMORY_DIR="$TMP/mem2"
|
|
96
|
+
"$BIN/soul-init" --memory-only >/dev/null 2>&1
|
|
97
|
+
[ ! -f "$MEMORY_DIR/SOUL.md" ] && ok "memory-only 不生成 SOUL.md" || bad "memory-only 还是生成了 SOUL"
|
|
98
|
+
grep -q "^status: skip" "$MEMORY_DIR/BOOTSTRAP.md" 2>/dev/null && ok "memory-only 把铸魂标成 skip" || bad "memory-only 铸魂还停在 pending(会反复催)"
|
|
99
|
+
"$BIN/soul-boot" --mode memory-only --list 2>&1 | grep -q "不注入(模式)" && ok "memory-only 不注入 SOUL" || bad "memory-only 仍在注入 SOUL"
|
|
100
|
+
# external:连 BOOTSTRAP 都不铺
|
|
101
|
+
MEMORY_DIR="$TMP/mem3" "$BIN/soul-init" --external >/dev/null 2>&1
|
|
102
|
+
[ ! -f "$TMP/mem3/BOOTSTRAP.md" ] && ok "external 不铺铸魂清单" || bad "external 还铺了 BOOTSTRAP"
|
|
103
|
+
MEMORY_DIR="$TMP/mem3" "$BIN/soul-boot" --mode external 2>&1 | grep -q "记忆库还是空的" && ok "external 空库时不硬塞东西" || bad "external 空库仍注入了内容"
|
|
104
|
+
# 空模板保护:没填的 SOUL 不许进上下文
|
|
105
|
+
MEMORY_DIR="$TMP/mem2" "$BIN/soul-boot" --keep-empty 2>&1 | grep -q "(待填" && ok "占位符确实存在(对照)" || true
|
|
106
|
+
MEMORY_DIR="$TMP/mem2" "$BIN/soul-boot" 2>&1 | grep -q "(待填" && bad "空模板被注入了" || ok "空 SOUL 不会进上下文"
|
|
107
|
+
unset MEMORY_DIR
|
|
108
|
+
|
|
109
|
+
echo "== 6. 插件加载 =="
|
|
110
|
+
export MEMORY_DIR="$TMP/home/.soul-forging/memory"
|
|
94
111
|
node -e "
|
|
95
112
|
import('$PREFIX/lib/node_modules/dsh-soul-forging/lib/plugin.js').then(m=>{
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
const
|
|
113
|
+
const memoryDir = process.env.MEMORY_DIR;
|
|
114
|
+
let c;
|
|
115
|
+
const ctx = {systemPrompt:{context(o){c=o;return()=>{}}}, skills:{register(){return()=>{}}}, logger:{info(){},warn(){}}};
|
|
116
|
+
m.apply(ctx, memoryDir ? {memoryDir} : {});
|
|
117
|
+
const t = c.text({});
|
|
99
118
|
if(typeof m.name!=='string') throw new Error('name 缺失');
|
|
100
119
|
if(!Array.isArray(m.inject)) throw new Error('inject 缺失');
|
|
101
|
-
if(!t || t.length<
|
|
120
|
+
if(!t || t.length<100) throw new Error('注入块太短: '+t.length);
|
|
121
|
+
const mm = m.bootBlock(memoryDir, 20000, { mode: 'memory-only' });
|
|
122
|
+
const ee = m.bootBlock(memoryDir, 20000, { mode: 'external' });
|
|
123
|
+
if(typeof mm!=='string' || typeof ee!=='string') throw new Error('模式参数不可用');
|
|
124
|
+
if(ee.includes('小蓝') && !t.includes('小蓝')) throw new Error('模式串了');
|
|
102
125
|
console.log('OK '+t.length);
|
|
103
126
|
}).catch(e=>{console.log('FAIL '+e.message);process.exit(1)})
|
|
104
127
|
" >/tmp/smoke-plugin.out 2>&1
|
package/soul.py
CHANGED
|
@@ -2,16 +2,26 @@
|
|
|
2
2
|
# -*- coding: utf-8 -*-
|
|
3
3
|
"""接线层:把记忆库接进任何 agent(dsh-soul-forging)。
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
三种用法:
|
|
6
6
|
|
|
7
|
-
①
|
|
8
|
-
soul-boot
|
|
9
|
-
soul-boot --budget 12000
|
|
10
|
-
soul-boot --list
|
|
7
|
+
① 命令行
|
|
8
|
+
soul-boot 打印开局注入块
|
|
9
|
+
soul-boot --budget 12000 限制字数(超了从末尾截)
|
|
10
|
+
soul-boot --list 看各部分多长、哪些被跳过
|
|
11
|
+
soul-boot --mode memory-only 只要记忆,不要性格
|
|
12
|
+
soul-boot --with index.md,NOW.md 自己挑要哪些文件
|
|
11
13
|
|
|
12
14
|
② 当模块用(Python)
|
|
13
|
-
from
|
|
14
|
-
sys_prompt += boot()
|
|
15
|
+
from soul import boot, recall, remember
|
|
16
|
+
sys_prompt += boot(mode="memory-only")
|
|
17
|
+
|
|
18
|
+
③ 直接读文件
|
|
19
|
+
就是那四个 markdown,自己拼也行
|
|
20
|
+
|
|
21
|
+
三种模式:
|
|
22
|
+
full 四个文件全注入(默认)—— 想长性格的人
|
|
23
|
+
memory-only 不注入 SOUL —— 只要记忆、不想动性格的人
|
|
24
|
+
external 只注入 index + NOW —— 性格交给你自己 agent 的 prompt
|
|
15
25
|
|
|
16
26
|
设计原则:**只读,不写**。注入不产生副作用;要写记忆走 remember()。
|
|
17
27
|
"""
|
|
@@ -23,7 +33,19 @@ DATA_ROOT = os.environ.get("SOUL_DATA_DIR") or os.path.join(os.path.expanduser("
|
|
|
23
33
|
MEM = os.environ.get("MEMORY_DIR") or os.path.join(DATA_ROOT, "memory")
|
|
24
34
|
|
|
25
35
|
# 注入顺序就是重要性顺序:先是谁,再是什么规矩,再是目录,最后是此刻。
|
|
26
|
-
|
|
36
|
+
ALL_PARTS = ("SOUL.md", "MEMORY.md", "index.md", "NOW.md")
|
|
37
|
+
|
|
38
|
+
MODES = {
|
|
39
|
+
"full": ALL_PARTS,
|
|
40
|
+
# 不要性格:SOUL 不注入。空模板塞进去比不塞更糟——模型会看到"我的名字是(待填)"
|
|
41
|
+
"memory-only": ("MEMORY.md", "index.md", "NOW.md"),
|
|
42
|
+
# 性格归你自己的 agent:我们只管目录和此刻
|
|
43
|
+
"external": ("index.md", "NOW.md"),
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
# 判断"这页还是空模板":占位符太多就别注入,省的把模型带偏
|
|
47
|
+
PLACEHOLDER_MARKS = ("(待填", "(待填", "TODO", "(示例", "(还没有")
|
|
48
|
+
PLACEHOLDER_MIN = 3
|
|
27
49
|
|
|
28
50
|
|
|
29
51
|
def _read(name):
|
|
@@ -35,26 +57,41 @@ def _read(name):
|
|
|
35
57
|
return ""
|
|
36
58
|
|
|
37
59
|
|
|
38
|
-
def
|
|
39
|
-
"""
|
|
60
|
+
def is_placeholder(body):
|
|
61
|
+
"""空模板检测:占位符出现次数够多,就当这页还没填。"""
|
|
62
|
+
return sum(body.count(m) for m in PLACEHOLDER_MARKS) >= PLACEHOLDER_MIN
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def boot(budget=None, parts=None, header=True, mode=None, skip_empty=True):
|
|
66
|
+
"""拼出开局注入块。
|
|
67
|
+
|
|
68
|
+
budget 字数上限(None = 不限制)
|
|
69
|
+
parts 指定要哪些文件(优先级高于 mode)
|
|
70
|
+
mode full / memory-only / external
|
|
71
|
+
skip_empty 自动跳过还是空模板的页(默认开)
|
|
72
|
+
"""
|
|
73
|
+
use = tuple(parts) if parts else MODES.get(mode or "full", ALL_PARTS)
|
|
40
74
|
chunks = []
|
|
41
75
|
total = 0
|
|
42
|
-
for name in
|
|
76
|
+
for name in use:
|
|
43
77
|
body = _read(name)
|
|
44
78
|
if not body:
|
|
45
79
|
continue
|
|
46
|
-
|
|
80
|
+
if skip_empty and is_placeholder(body):
|
|
81
|
+
continue
|
|
47
82
|
block = f"<!-- {name} -->\n{body}"
|
|
48
83
|
if budget is not None and total + len(block) > budget:
|
|
49
84
|
left = budget - total
|
|
50
85
|
if left > 200: # 剩一点就截,剩太少就整块不要
|
|
51
|
-
block
|
|
52
|
-
|
|
53
|
-
total += len(block)
|
|
86
|
+
chunks.append(block[:left] + "\n…(本块超预算被截断)")
|
|
87
|
+
total += left + 20
|
|
54
88
|
break
|
|
55
89
|
chunks.append(block)
|
|
56
90
|
total += len(block)
|
|
57
91
|
body = "\n\n".join(chunks)
|
|
92
|
+
if not body:
|
|
93
|
+
# 什么都没填时不硬塞一句空话,明说
|
|
94
|
+
return "(记忆库还是空的,暂时没有可注入的内容。)" if header else ""
|
|
58
95
|
if not header:
|
|
59
96
|
return body
|
|
60
97
|
return (
|
|
@@ -91,16 +128,37 @@ def _main(argv):
|
|
|
91
128
|
budget = None
|
|
92
129
|
if "--budget" in argv:
|
|
93
130
|
budget = int(argv[argv.index("--budget") + 1])
|
|
131
|
+
mode = None
|
|
132
|
+
if "--mode" in argv:
|
|
133
|
+
mode = argv[argv.index("--mode") + 1]
|
|
134
|
+
if mode not in MODES:
|
|
135
|
+
print(f"不认识的模式:{mode}(可选:{', '.join(MODES)})")
|
|
136
|
+
return 1
|
|
137
|
+
parts = None
|
|
138
|
+
if "--with" in argv:
|
|
139
|
+
parts = tuple(x.strip() for x in argv[argv.index("--with") + 1].split(",") if x.strip())
|
|
140
|
+
skip_empty = "--keep-empty" not in argv
|
|
141
|
+
|
|
94
142
|
if "--list" in argv:
|
|
95
|
-
|
|
143
|
+
use = parts or MODES.get(mode or "full", ALL_PARTS)
|
|
144
|
+
print(f"模式:{mode or 'full'}")
|
|
145
|
+
for name in ALL_PARTS:
|
|
96
146
|
body = _read(name)
|
|
97
|
-
|
|
98
|
-
|
|
147
|
+
if name not in use:
|
|
148
|
+
tag = "不注入(模式)"
|
|
149
|
+
elif not body:
|
|
150
|
+
tag = "(空)"
|
|
151
|
+
elif skip_empty and is_placeholder(body):
|
|
152
|
+
tag = "空模板,已跳过"
|
|
153
|
+
else:
|
|
154
|
+
tag = "✓"
|
|
155
|
+
print(f" {name:16s} {len(body):6d} 字 {tag}")
|
|
99
156
|
return 0
|
|
157
|
+
|
|
100
158
|
if "--bare" in argv:
|
|
101
|
-
print(boot(budget=budget, header=False))
|
|
159
|
+
print(boot(budget=budget, parts=parts, header=False, mode=mode, skip_empty=skip_empty))
|
|
102
160
|
return 0
|
|
103
|
-
print(boot(budget=budget))
|
|
161
|
+
print(boot(budget=budget, parts=parts, mode=mode, skip_empty=skip_empty))
|
|
104
162
|
return 0
|
|
105
163
|
|
|
106
164
|
|