dsh-soul-forging 0.1.5 → 0.1.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.
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,57 @@ mkdir -p "$TARGET"
16
30
  cp -a "$MEMKIT_ROOT/memory/." "$TARGET/"
17
31
  # 数据目录也建出来(索引和服务写这里,别写进包目录)
18
32
  mkdir -p "${BRAIN_DIR:-$(dirname "$TARGET")/brain}"
19
- echo "✓ 空白记忆库已铺到:$TARGET"
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
+ sed -i 's/^status: pending$/status: skip/' "$TARGET/BOOTSTRAP.md" 2>/dev/null || \
58
+ python3 - "$TARGET/BOOTSTRAP.md" <<'PY' 2>/dev/null || true
59
+ import re, sys
60
+ p = sys.argv[1]
61
+ s = open(p, encoding="utf-8").read()
62
+ open(p, "w", encoding="utf-8").write(re.sub(r"^status: \w+$", "status: skip", s, count=1, flags=re.M))
63
+ PY
64
+ fi
65
+
66
+ echo "✓ 空白记忆库已铺到:$TARGET(模式:$MODE)"
20
67
  echo
21
- echo "接下来:"
22
- echo " 1. 填 $TARGET/SOUL.md"
23
- echo " 2. 按 $TARGET/BOOTSTRAP.md 走一遍铸魂"
24
- echo " 3. 建索引:python3 $MEMKIT_ROOT/brain/index.py"
25
- echo " 4. 起服务:bash $MEMKIT_ROOT/bin/serve"
68
+ case "$MODE" in
69
+ full)
70
+ echo "接下来:"
71
+ echo " 1. 填 $TARGET/SOUL.md"
72
+ echo " 2. 按 $TARGET/BOOTSTRAP.md 走一遍铸魂"
73
+ ;;
74
+ memory-only)
75
+ echo "接下来:直接开始记东西就行,不用管性格。"
76
+ echo " · 注入会自动跳过空的 SOUL"
77
+ echo " · 想改主意:把 $TARGET/BOOTSTRAP.md 的 status 改回 pending"
78
+ ;;
79
+ external)
80
+ echo "接下来:性格由你自己的 agent prompt 负责,这里只管记忆。"
81
+ echo " · 注入只有 index.md + NOW.md"
82
+ ;;
83
+ esac
84
+ echo " · 建索引:python3 $MEMKIT_ROOT/brain/index.py"
85
+ echo " · 起服务:bash $MEMKIT_ROOT/bin/serve"
86
+ echo " · 看注入:soul-boot --list"
package/bin/sync CHANGED
@@ -10,10 +10,15 @@ if ! flock -n 9; then echo "同步已在运行,跳过"; exit 0; fi
10
10
 
11
11
  R() { bash "$BIN_DIR/runlog" "$@"; }; rc=0
12
12
  bash "$BIN_DIR/runlog" "live-summary" -- "$PY" "$MEMKIT_ROOT/brain/fix-live-summary.py" || rc=$((rc+1))
13
- bash "$BIN_DIR/runlog" "archive-log" -- bash "$BIN_DIR/archive-log" || rc=$((rc+2))
13
+ bash "$BIN_DIR/runlog" "archive-log" -- "$PY" "$BIN_DIR/archive-log" || rc=$((rc+2))
14
14
  bash "$BIN_DIR/runlog" "index" -- "$PY" "$MEMKIT_ROOT/brain/index.py" || rc=$((rc+4))
15
- bash "$BIN_DIR/runlog" "reload" -- bash -c "curl -s -m 5 -X POST $BRAIN_URL/reload >/dev/null" || rc=$((rc+8))
16
- bash "$BIN_DIR/runlog" "check" -- bash "$BIN_DIR/soul" check || rc=$((rc+16))
15
+ # 服务没起就不算失败 —— 用户不一定要常驻它,检索会自己现算
16
+ if curl -s -m 2 "$BRAIN_URL/health" 2>/dev/null | grep -q '"ok"'; then
17
+ bash "$BIN_DIR/runlog" "reload" -- bash -c "curl -s -m 5 -X POST $BRAIN_URL/reload >/dev/null" || rc=$((rc+8))
18
+ else
19
+ echo "(检索服务没起,跳过重载——不影响使用)"
20
+ fi
21
+ bash "$BIN_DIR/runlog" "check" -- "$PY" "$BIN_DIR/soul" check || rc=$((rc+16))
17
22
  [ -n "${SOUL_BACKUP_CMD:-}" ] && { bash "$BIN_DIR/runlog" "backup" -- bash -c "$SOUL_BACKUP_CMD" || rc=$((rc+32)); }
18
23
 
19
24
  if [ $rc -eq 0 ]; then echo "sync ok"; else echo "sync 有失败项 rc=$rc(看 $BRAIN_DIR/alarm.log)"; fi
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 PARTS = ['SOUL.md', 'MEMORY.md', 'index.md', 'NOW.md']
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 PARTS) {
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}(上限 ${budget} 字)`)
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 { PARTS, KIT_ROOT }
183
+ export { ALL_PARTS, MODES, KIT_ROOT }
@@ -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
- 1. `status` 未变成 `complete` 时,每次会话开始的头一件事是**继续这个对话**,不埋头做别的。
40
+ 0. **`status: skip` 时整份清单作废**——用户明确说了不要性格,别再问他"你叫什么名字"。
41
+ 什么时候想动了,把 status 改回 `pending` 就重新生效。
42
+ 1. `status` 未变成 `complete`(且不是 `skip`)时,每次会话开始的头一件事是**继续这个对话**,不埋头做别的。
41
43
  主人明确说要先干别的,就照做,但之后要回来。
42
44
  2. 一次推进一项。**不要一口气问完**——那是问卷,不是聊天。
43
45
  3. 写回文件之后才勾选,顺序不能反。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-soul-forging",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
4
4
  "description": "Separate Vessels, One Heart · 异体同心 —— 给 AI 的可迁移长期记忆与铸魂流程:markdown 记忆库 + 小脑检索 + 能长出来的性格。纯本地,不要服务器。",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/scripts/smoke.sh CHANGED
@@ -58,9 +58,16 @@ run() { # 名字 命令...
58
58
 
59
59
  run "soul-init" "$BIN/soul-init"
60
60
  run "soul-boot" "$BIN/soul-boot" --list
61
- run "soul-sync" "$BIN/soul-sync"
61
+ # sync 永远 exit 0(失败也返回 0,只是打印"有失败项"),所以必须看输出
62
+ sync_ok() {
63
+ local out; out=$("$BIN/soul-sync" 2>&1)
64
+ echo "$out" | grep -q "^sync ok$" && ok "soul-sync(干净)" && return 0
65
+ bad "soul-sync 有失败项" "$(echo "$out" | grep -E '失败' | head -1)"
66
+ return 1
67
+ }
68
+ sync_ok
62
69
  run "soul (write)" "$BIN/soul" write "烟测写的一条"
63
- run "soul-sync 2" "$BIN/soul-sync"
70
+ sync_ok
64
71
  run "soul-recall" "$BIN/soul-recall" "烟测" 1
65
72
  echo "$LAST_OUT" | grep -q "烟测写的一条" && ok "检索命中刚写的内容" || bad "检索没命中"
66
73
 
@@ -69,7 +76,7 @@ PORT=$(( (RANDOM % 2000) + 39000 ))
69
76
  BRAIN_PORT=$PORT "$BIN/soul-serve" >/dev/null 2>&1
70
77
  sleep 1
71
78
  curl -s -m 3 "http://127.0.0.1:$PORT/health" | grep -q '"ok": true' && ok "soul-serve 起得来" || bad "soul-serve"
72
- BRAIN_PORT=$PORT "$BIN/soul-sync" >/dev/null 2>&1 && ok "sync(服务在跑时)" || bad "sync(服务在跑时)"
79
+ BRAIN_PORT=$PORT "$BIN/soul-sync" 2>&1 | grep -q "^sync ok$" && ok "sync(服务在跑时)" || bad "sync(服务在跑时)"
73
80
 
74
81
  run "soul-check" "$BIN/soul" check
75
82
  run "soul-forget" "$BIN/soul-forget"
@@ -83,15 +90,37 @@ run "soul-debug" "$BIN/soul-debug"
83
90
  echo "== 4. 包目录必须干净(跑完所有命令之后查)=="
84
91
  DIRTY=$(check_pkg_clean) && ok "包目录没被写脏" || bad "包目录被污染:$DIRTY"
85
92
 
86
- 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
+ "$BIN/soul-boot" --mode memory-only --list 2>&1 | grep -q "不注入(模式)" && ok "memory-only 不注入 SOUL" || bad "memory-only 仍在注入 SOUL"
99
+ # external:连 BOOTSTRAP 都不铺
100
+ MEMORY_DIR="$TMP/mem3" "$BIN/soul-init" --external >/dev/null 2>&1
101
+ [ ! -f "$TMP/mem3/BOOTSTRAP.md" ] && ok "external 不铺铸魂清单" || bad "external 还铺了 BOOTSTRAP"
102
+ MEMORY_DIR="$TMP/mem3" "$BIN/soul-boot" --mode external 2>&1 | grep -q "记忆库还是空的" && ok "external 空库时不硬塞东西" || bad "external 空库仍注入了内容"
103
+ # 空模板保护:没填的 SOUL 不许进上下文
104
+ MEMORY_DIR="$TMP/mem2" "$BIN/soul-boot" --keep-empty 2>&1 | grep -q "(待填" && ok "占位符确实存在(对照)" || true
105
+ MEMORY_DIR="$TMP/mem2" "$BIN/soul-boot" 2>&1 | grep -q "(待填" && bad "空模板被注入了" || ok "空 SOUL 不会进上下文"
106
+ unset MEMORY_DIR
107
+
108
+ echo "== 6. 插件加载 =="
109
+ export MEMORY_DIR="$TMP/home/.soul-forging/memory"
87
110
  node -e "
88
111
  import('$PREFIX/lib/node_modules/dsh-soul-forging/lib/plugin.js').then(m=>{
89
- let c; const ctx={systemPrompt:{context(o){c=o;return()=>{}}},skills:{register(){return()=>{}}},logger:{info(){},warn(){}}};
90
- m.apply(ctx,{});
91
- const t=c.text({});
112
+ const memoryDir = process.env.MEMORY_DIR;
113
+ let c;
114
+ const ctx = {systemPrompt:{context(o){c=o;return()=>{}}}, skills:{register(){return()=>{}}}, logger:{info(){},warn(){}}};
115
+ m.apply(ctx, memoryDir ? {memoryDir} : {});
116
+ const t = c.text({});
92
117
  if(typeof m.name!=='string') throw new Error('name 缺失');
93
118
  if(!Array.isArray(m.inject)) throw new Error('inject 缺失');
94
- if(!t || t.length<1000) throw new Error('注入块太短: '+t.length);
119
+ if(!t || t.length<100) throw new Error('注入块太短: '+t.length);
120
+ const mm = m.bootBlock(memoryDir, 20000, { mode: 'memory-only' });
121
+ const ee = m.bootBlock(memoryDir, 20000, { mode: 'external' });
122
+ if(typeof mm!=='string' || typeof ee!=='string') throw new Error('模式参数不可用');
123
+ if(ee.includes('小蓝') && !t.includes('小蓝')) throw new Error('模式串了');
95
124
  console.log('OK '+t.length);
96
125
  }).catch(e=>{console.log('FAIL '+e.message);process.exit(1)})
97
126
  " >/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 kit import boot, recall, remember
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
- PARTS = ("SOUL.md", "MEMORY.md", "index.md", "NOW.md")
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 boot(budget=None, parts=PARTS, header=True):
39
- """拼出开局注入块。budget 是字数上限(None = 不限制)。"""
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 parts:
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 = block[:left] + "\n…(本块超预算被截断)"
52
- chunks.append(block)
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
- for name in PARTS:
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
- print(f"{name:16s} {len(body):6d} 字 {'✓' if body else '(空)'}")
98
- print(f"{'合计':16s} {sum(len(_read(n)) for n in PARTS):6d} 字")
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