@bolloon/bolloon-agent 0.3.27 → 0.3.28

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.
@@ -0,0 +1,213 @@
1
+ /**
2
+ * context-os.ts — Context OS 资产层 (2026-08-03, P5)
3
+ *
4
+ * 把 Ziye-Context-OS 的 12+3 层文件夹体系落地到 Bolloon:
5
+ * ~/.bolloon/context-os/
6
+ * 01-Me ~ 12-Analysis + output / research / tmp
7
+ *
8
+ * 每层回答一种问题 (Context OS §3):
9
+ * 01-Me 我是谁 / 02-Network 我认识谁 / 03-Current 我现在在做什么
10
+ * 04-Projects 我正在推进什么 / 05-Prompts 哪些提示词已验证
11
+ * 06-Protocols AI 和系统该如何工作 / 07-Knowledge 哪些知识跨项目复用
12
+ * 08-Insights 哪些判断改变决策 / 09-Tools 哪些工具省时间
13
+ * 10-Skills 哪些能力可验证 / 11-Write 哪些表达可复用
14
+ * 12-Analysis 决策过程与复盘 / output 对外交付 / research 中间成果 / tmp 一次性草稿
15
+ *
16
+ * 价值判断标准 (Context OS §5): 每个资产进入前回答"未来哪个具体场景会用到它?".
17
+ * 回答不出 = 噪音, 不该进正式层.
18
+ *
19
+ * 设计 (减法):
20
+ * - 每层一个 README.md 声明职责边界 (存什么/不该存什么/典型用途)
21
+ * - 资产文件: <ts>-<slug>.md, frontmatter v2 (stage0 = 临时价值点, 与 judgeness 生命周期对应)
22
+ * - 写操作失败静默, 不阻塞主对话
23
+ */
24
+ import * as fs from 'fs/promises';
25
+ import * as os from 'os';
26
+ import * as path from 'path';
27
+ export const CONTEXT_OS_LAYERS = [
28
+ { key: '01-Me', name: '我是谁', store: '经过验证的原则、不可碰的边界、稳定偏好', notStore: '临时情绪、未经验证的念头', usage: '防止 AI 用错误的方式帮助你' },
29
+ { key: '02-Network', name: '我认识谁', store: '有真实关系、能力可定位、能在具体问题上调用的人', notStore: '只看过主页的陌生人', usage: '需要咨询、合作、求证时找到正确的人' },
30
+ { key: '03-Current', name: '我现在在做什么', store: '今天/本周的现实状态、工作现场、阻塞项', notStore: '长期知识、项目历史全文', usage: '防止 AI 按过期状态给建议' },
31
+ { key: '04-Projects', name: '我正在推进什么', store: '有明确交付、真实进度、可验证证据的项目', notStore: '只有想法的脑暴', usage: '让 AI 按项目真实边界推进' },
32
+ { key: '05-Prompts', name: '已验证可复用的提示词', store: '至少复用过、效果稳定的 Prompt', notStore: '一次性调试 Prompt', usage: '跨项目、跨工具复用工作方法' },
33
+ { key: '06-Protocols', name: 'AI 和系统该如何工作', store: '为防止真实错误而产生、被重复调用的规则', notStore: '纯理论流程', usage: '把"知道"变成"每次都会做"' },
34
+ { key: '07-Knowledge', name: '哪些领域知识未来复用', store: '未来至少三个项目可能复用的领域理解', notStore: '随手可搜的常识', usage: '技术/行业问题的长期积累' },
35
+ { key: '08-Insights', name: '哪些已验证的判断改变决策', store: '能改变决策、产品方向或自我认知的已验证判断', notStore: '情绪碎片、未经验证的直觉', usage: '防止重复踩同一种坑' },
36
+ { key: '09-Tools', name: '哪些工具和脚本省时间', store: '实际用过、能节约时间、包含回退方案的工具经验', notStore: '只安装未使用的软件', usage: '提升执行效率,避免重复试错' },
37
+ { key: '10-Skills', name: '哪些能力可验证交付', store: '可外部验证、能交付、有作品支撑的能力', notStore: '"我想学"的愿望', usage: '简历、分工、能力缺口识别' },
38
+ { key: '11-Write', name: '哪些写作可复用', store: '可以引用、改写、发布的成熟表达', notStore: '未整理草稿', usage: '保持跨场景表达一致' },
39
+ { key: '12-Analysis', name: '决策过程与复盘', store: '有推理链、可事后复盘的研究与决策', notStore: '只有结论的事后合理化', usage: '重要决策可追溯、可修正' },
40
+ { key: 'output', name: '对外交付物', store: '给外部的人看的最终交付物', notStore: '内部草稿', usage: '可直接分享给他人' },
41
+ { key: 'research', name: '研究中间成果', store: '研究中的中间成果', notStore: '结论已定型的资产', usage: '未完成研究的暂存' },
42
+ { key: 'tmp', name: '一次性草稿', store: '一次性草稿与临时文件', notStore: '任何未来要复用的东西', usage: '定期清理' },
43
+ ];
44
+ const LAYER_KEYS = new Set(CONTEXT_OS_LAYERS.map((l) => l.key));
45
+ // ============================================================
46
+ // 路径
47
+ // ============================================================
48
+ export function getContextOsRoot(home = os.homedir()) {
49
+ return path.join(home, '.bolloon', 'context-os');
50
+ }
51
+ export function getLayerDir(layer, home = os.homedir()) {
52
+ return path.join(getContextOsRoot(home), layer);
53
+ }
54
+ /** 校验 layer 合法; 非法返回 null */
55
+ export function resolveLayer(layer) {
56
+ const key = String(layer || '').trim();
57
+ return LAYER_KEYS.has(key) ? CONTEXT_OS_LAYERS.find((l) => l.key === key) : null;
58
+ }
59
+ function slugify(s) {
60
+ return s.replace(/[^a-zA-Z0-9\u4e00-\u9fa5_-]/g, '_').slice(0, 40) || 'untitled';
61
+ }
62
+ // ============================================================
63
+ // 初始化: 建目录 + 每层 README (幂等)
64
+ // ============================================================
65
+ function layerReadme(l) {
66
+ return `# ${l.key} — ${l.name}
67
+
68
+ ## 这一层回答的问题
69
+ ${l.name}
70
+
71
+ ## 存什么
72
+ ${l.store}
73
+
74
+ ## 不该存什么
75
+ ${l.notStore}
76
+
77
+ ## 典型用途
78
+ ${l.usage}
79
+
80
+ ## 价值判断标准 (Context OS §5)
81
+ 写入前先回答: **未来哪个具体场景会用到它?**
82
+ 回答不出 = 噪音, 留在 tmp/, 不进正式层.
83
+
84
+ ## 价值生命周期
85
+ 阶段0 临时价值点 (对话中刚出现, 未验证) → 阶段1 验证 (被使用/确认)
86
+ → 阶段2 固化 (本层唯一位置) → 阶段3 索引化 (高频引用) → 阶段4 归档/删除.
87
+ `;
88
+ }
89
+ export async function ensureContextOsDirs(home) {
90
+ const root = getContextOsRoot(home);
91
+ await fs.mkdir(root, { recursive: true });
92
+ for (const l of CONTEXT_OS_LAYERS) {
93
+ const dir = getLayerDir(l.key, home);
94
+ await fs.mkdir(dir, { recursive: true });
95
+ const readmePath = path.join(dir, 'README.md');
96
+ try {
97
+ await fs.access(readmePath);
98
+ }
99
+ catch {
100
+ await fs.writeFile(readmePath, layerReadme(l), 'utf-8');
101
+ }
102
+ }
103
+ }
104
+ /**
105
+ * 写入资产到指定层.
106
+ * 文件名: <ts>-<slug>.md; frontmatter v2 (stage0 = 临时价值点, 待验证).
107
+ * 幂等: 同层同 slug 已存在 → 跳过 (不重复造文件, Context OS §6 Step3).
108
+ */
109
+ export async function writeContextAsset(input, home) {
110
+ const layer = resolveLayer(input.layer);
111
+ if (!layer) {
112
+ return { ok: false, error: `layer 非法: '${input.layer}'. 合法: ${CONTEXT_OS_LAYERS.map((l) => l.key).join(' / ')}` };
113
+ }
114
+ const title = String(input.title || '').trim();
115
+ if (!title)
116
+ return { ok: false, error: 'title 必填' };
117
+ const content = String(input.content || '').trim();
118
+ if (!content)
119
+ return { ok: false, error: 'content 必填' };
120
+ await ensureContextOsDirs(home);
121
+ const now = new Date().toISOString();
122
+ const ts = Date.now();
123
+ const slug = slugify(title);
124
+ const fileName = `${ts}-${slug}.md`;
125
+ const filePath = path.join(getLayerDir(layer.key, home), fileName);
126
+ // 幂等: 同 slug 已存在 → 跳过
127
+ try {
128
+ const files = await fs.readdir(getLayerDir(layer.key, home));
129
+ if (files.some((f) => f.endsWith(`-${slug}.md`))) {
130
+ return { ok: true, skipped: true, error: `同标题资产已存在 (${slug}.md), 未重复写入` };
131
+ }
132
+ }
133
+ catch { /* 目录不存在, 继续 */ }
134
+ const fm = [
135
+ '---',
136
+ `title: ${title.replace(/[\n\r]/g, ' ').slice(0, 80)}`,
137
+ `source: session`,
138
+ `created: ${now}`,
139
+ `layer: ${layer.key}`,
140
+ `stage: stage0`,
141
+ `tags: [${(input.tags || []).map((t) => t.replace(/[^\w\u4e00-\u9fa5-]/g, '')).filter(Boolean).join(', ')}]`,
142
+ input.domain ? `domain: ${input.domain.replace(/[\n\r]/g, ' ').slice(0, 40)}` : '',
143
+ 'schema_version: 2',
144
+ '---',
145
+ '',
146
+ content,
147
+ ].filter(Boolean).join('\n');
148
+ try {
149
+ await fs.writeFile(filePath, fm, 'utf-8');
150
+ return {
151
+ ok: true,
152
+ asset: { layer: layer.key, file: fileName, title, path: filePath, createdAt: now, stage: 'stage0' },
153
+ };
154
+ }
155
+ catch (e) {
156
+ return { ok: false, error: `写入失败: ${e?.message || String(e)}` };
157
+ }
158
+ }
159
+ /** 列出层资产; layer 为空 → 全层汇总 */
160
+ export async function readContextAssets(layer, keyword, home) {
161
+ const root = getContextOsRoot(home);
162
+ const kw = String(keyword || '').trim().toLowerCase();
163
+ const wanted = layer ? [resolveLayer(layer)].filter(Boolean).map((l) => l.key) : CONTEXT_OS_LAYERS.map((l) => l.key);
164
+ const out = [];
165
+ for (const key of wanted) {
166
+ const l = resolveLayer(key);
167
+ try {
168
+ const files = (await fs.readdir(getLayerDir(key, home))).filter((f) => f.endsWith('.md') && f !== 'README.md');
169
+ const entries = [];
170
+ for (const f of files) {
171
+ try {
172
+ const raw = await fs.readFile(path.join(getLayerDir(key, home), f), 'utf-8');
173
+ const titleM = raw.match(/^title:\s*(.+)$/m);
174
+ const createdM = raw.match(/^created:\s*(.+)$/m);
175
+ const title = titleM ? titleM[1].trim() : f.replace(/\.md$/, '');
176
+ if (kw && !(title.toLowerCase().includes(kw) || raw.toLowerCase().includes(kw)))
177
+ continue;
178
+ entries.push({ file: f, title, createdAt: createdM ? createdM[1].trim() : '' });
179
+ }
180
+ catch { /* 单文件损坏跳过 */ }
181
+ }
182
+ entries.sort((a, b) => b.createdAt.localeCompare(a.createdAt));
183
+ out.push({ layer: key, name: l.name, fileCount: entries.length, files: entries.slice(0, 20) });
184
+ }
185
+ catch {
186
+ out.push({ layer: key, name: l.name, fileCount: 0, files: [] });
187
+ }
188
+ }
189
+ return out;
190
+ }
191
+ /** 读取单篇资产正文 (供工具输出完整内容) */
192
+ export async function readAssetBody(layer, file, home) {
193
+ const l = resolveLayer(layer);
194
+ if (!l)
195
+ return { ok: false, error: `layer 非法: '${layer}'` };
196
+ const safeFile = path.basename(String(file || '').replace(/[^\w\u4e00-\u9fa5.-]/g, '_'));
197
+ if (!safeFile.endsWith('.md'))
198
+ return { ok: false, error: 'file 必须是 .md' };
199
+ try {
200
+ const raw = await fs.readFile(path.join(getLayerDir(l.key, home), safeFile), 'utf-8');
201
+ return { ok: true, body: raw };
202
+ }
203
+ catch (e) {
204
+ return { ok: false, error: `读取失败: ${e?.message || String(e)}` };
205
+ }
206
+ }
207
+ /** 层 → 上下文注入摘要 (给 LLM 的资产层目录) */
208
+ export function formatLayerListing(listings) {
209
+ if (listings.length === 0)
210
+ return '';
211
+ const lines = listings.map((l) => ` - ${l.layer} (${l.name}): ${l.fileCount} 篇` + (l.files.length > 0 ? ` — ${l.files.slice(0, 3).map((f) => f.title).join(' / ')}` : ''));
212
+ return `[系统上下文] 资产层 (Context OS 12+3 层, 先看 03-Current 再按任务路由):\n${lines.join('\n')}\n\n`;
213
+ }
@@ -9,7 +9,7 @@ import * as os from 'os';
9
9
  import * as path from 'path';
10
10
  import { getCachedBolloonContext, clearBolloonContextCache } from './context-collector.js';
11
11
  import { formatContextForSystemPrompt } from './project-context.js';
12
- import { loadPersonaDocs, formatPersonaForSystemPrompt } from './persona-loader.js';
12
+ import { loadPersonaDocs, formatPersonaForSystemPrompt, loadPersonaJudgmentDeclaration, formatJudgmentDeclaration } from './persona-loader.js';
13
13
  let lastSessionStartAt = 0;
14
14
  const MIN_INTERVAL_MS = 5000; // 同一进程 5s 内最多触发一次, 防止循环
15
15
  export async function onSessionStart(opts = {}) {
@@ -30,7 +30,13 @@ export async function onSessionStart(opts = {}) {
30
30
  if (opts.agentId) {
31
31
  try {
32
32
  const docs = await loadPersonaDocs(opts.agentId);
33
- const personaText = formatPersonaForSystemPrompt(docs);
33
+ let personaText = formatPersonaForSystemPrompt(docs);
34
+ // 2026-08-03 (Context OS P1): 追加 persona frontmatter 里的判断力声明
35
+ // (judgment_style / stakes_default / revisable) — 与 judgeness 5 维对应
36
+ const decl = await loadPersonaJudgmentDeclaration(opts.agentId);
37
+ const declText = formatJudgmentDeclaration(decl);
38
+ if (declText)
39
+ personaText = personaText ? `${personaText}\n\n${declText}` : declText;
34
40
  if (personaText) {
35
41
  systemAddition = personaText + '\n\n' + systemAddition;
36
42
  }
@@ -144,7 +144,8 @@ export async function compressSessionToMemory(opts) {
144
144
  const timestamp = new Date().toISOString();
145
145
  let summaryBody;
146
146
  try {
147
- const sysPrompt = '你是 bolloon 记忆压缩助手. 输入是一段 session 消息历史 (用户问题 + AI 回答), 输出 200-400 字中文摘要, 包含 3-5 条关键发现和未完成事项. 不要寒暄, 不要复述已知. 格式: ## 关键发现 / ## 待办.';
147
+ // 2026-08-03 (Context OS P4): 摘要 prompt 要求输出"价值点"段 供收尾路由入库 judgeness
148
+ const sysPrompt = '你是 bolloon 记忆压缩助手. 输入是一段 session 消息历史 (用户问题 + AI 回答), 输出 200-400 字中文摘要, 包含 3-5 条关键发现和未完成事项. 不要寒暄, 不要复述已知. 格式: ## 关键发现 / ## 待办. 最后单独输出 ## 价值点 段: 0-3 行, 每行 `- (类型) 一句话内容`, 类型 ∈ decision|lesson|knowledge|insight (decision=做出了什么决定; lesson=哪里出错下次怎么避免; knowledge=修正了什么认知; insight=改变了判断的洞察). 没有就写 `- (无)`';
148
149
  const recentSnippet = newMessages.slice(-10).map(m => `[${m.type}] ${m.content}`).join('\n---\n').slice(0, 6000);
149
150
  const userPrompt = `Channel: ${opts.channelId}\nSession: ${opts.sessionId}\n时间: ${timestamp}\n新增消息数: ${newMessages.length}\n\n最近消息:\n${recentSnippet}`;
150
151
  summaryBody = await tryLlmSummary(sysPrompt, userPrompt);
@@ -161,6 +162,18 @@ export async function compressSessionToMemory(opts) {
161
162
  await fs.mkdir(path.dirname(summaryPath), { recursive: true });
162
163
  await fs.appendFile(summaryPath, block, 'utf-8');
163
164
  await writeCursor(cursorPath, allMessages.length);
165
+ // 2026-08-03 (Context OS P4): 价值点分类路由 — 把摘要里的 decision/lesson/knowledge/insight
166
+ // 自动写入 human-values + judgeness (Context OS §6 对话收尾: 价值不流失).
167
+ // 失败静默, 不阻塞主对话. 幂等: 相同 decision 文本跳过.
168
+ try {
169
+ await routeValuePointsToJudgeness({
170
+ agentId,
171
+ channelId: opts.channelId,
172
+ summaryBody,
173
+ home,
174
+ });
175
+ }
176
+ catch { /* 静默 */ }
164
177
  // 2026-07-22 设计 C: 废气采样 — 压缩成功 = 上下文需要压缩的信号, 记入涡轮 (隐式)
165
178
  // 废气不进 prompt, 只调参 (背压高 → judgment 注入收紧). 落 log/memory.
166
179
  try {
@@ -179,3 +192,115 @@ export async function compressSessionToMemory(opts) {
179
192
  bytesWritten: Buffer.byteLength(block, 'utf-8'),
180
193
  };
181
194
  }
195
+ /**
196
+ * 解析摘要里的 `## 价值点` 段.
197
+ * 容错 3 种行格式: `- (decision) 内容` / `- decision: 内容` / `- decision 内容`
198
+ * 无该段 / `- (无)` → 返回 [].
199
+ */
200
+ export function extractValuePoints(summaryBody) {
201
+ if (!summaryBody)
202
+ return [];
203
+ const m = summaryBody.match(/##\s*价值点\s*\n([\s\S]*?)(?=\n##\s|\n---\s*$|$)/);
204
+ if (!m)
205
+ return [];
206
+ const lines = m[1].split('\n').map((l) => l.trim()).filter(Boolean);
207
+ const out = [];
208
+ const typeSet = ['decision', 'lesson', 'knowledge', 'insight'];
209
+ for (const line of lines) {
210
+ const stripped = line.replace(/^[-*•]\s*/, '');
211
+ if (stripped === '(无)' || stripped === '无' || stripped === '')
212
+ continue;
213
+ // - (type) content | - type: content | - type content
214
+ const m2 = stripped.match(/^\(?(\w+)\)?\s*[::]?\s+(.+)$/);
215
+ if (!m2)
216
+ continue;
217
+ const t = m2[1].toLowerCase();
218
+ if (!typeSet.includes(t))
219
+ continue;
220
+ const content = m2[2].trim();
221
+ if (content.length < 4)
222
+ continue;
223
+ out.push({ type: t, content });
224
+ }
225
+ return out.slice(0, 3);
226
+ }
227
+ /** 幂等检查: human-values 里已有相同 decision 文本 → 跳过 (防重复入库) */
228
+ async function alreadyRouted(decisionText, home) {
229
+ try {
230
+ const { loadAllJudgments } = await import('../pi-ecosystem-judgment/human-value-store.js');
231
+ const all = await loadAllJudgments();
232
+ return all.some((j) => String(j.decision).trim() === decisionText.trim());
233
+ }
234
+ catch {
235
+ return false; // 读失败 → 不跳过 (重试语义)
236
+ }
237
+ }
238
+ /**
239
+ * 把摘要里的价值点路由到 human-values + judgeness + Context OS 资产层.
240
+ * 失败静默 (由调用方 try/catch), 单条失败不影响其余.
241
+ * 落点 (Context OS §6 Step2 唯一落点):
242
+ * decision → decisions/ (decision-store, 不重复写资产层)
243
+ * lesson → human-values + judgeness + 12-Analysis/ (复盘)
244
+ * knowledge→ human-values + judgeness + 07-Knowledge/
245
+ * insight → human-values + judgeness + 08-Insights/
246
+ * 返回写入条数 (测试/日志用).
247
+ */
248
+ export async function routeValuePointsToJudgeness(opts) {
249
+ const points = extractValuePoints(opts.summaryBody);
250
+ if (points.length === 0)
251
+ return 0;
252
+ let written = 0;
253
+ for (const p of points) {
254
+ try {
255
+ const decisionText = p.content.slice(0, 300);
256
+ if (await alreadyRouted(decisionText, opts.home))
257
+ continue;
258
+ const { storeHumanJudgment } = await import('../pi-ecosystem-judgment/human-value-store.js');
259
+ const { reflectAfterJudgment } = await import('../judgeness/reflect.js');
260
+ const isLesson = p.type === 'lesson';
261
+ const judgment = await storeHumanJudgment({
262
+ decision: decisionText,
263
+ decision_type: isLesson ? 'reject' : 'approve',
264
+ reasons: [`来源: session 摘要价值点 (${p.type})`],
265
+ values_derived: [],
266
+ context: {
267
+ domain: opts.channelId?.startsWith('ch_') ? '通用' : (opts.channelId || '通用'),
268
+ complexity: 'simple',
269
+ stakes: 'low',
270
+ time_pressure: 'low',
271
+ },
272
+ metadata: {
273
+ source: isLesson ? 'trajectory' : 'implicit',
274
+ confidence: 0.6,
275
+ revisable: true,
276
+ },
277
+ status: 'active',
278
+ appliesTo: [],
279
+ });
280
+ await reflectAfterJudgment(judgment, 'agent', sanitizeAgentId(opts.agentId)).catch(() => null);
281
+ written += 1;
282
+ // 2026-08-03 (Context OS P5): 唯一落点 — knowledge/insight/lesson 写入资产层
283
+ // (幂等: 同标题已存在则跳过; 失败静默不影响主流程)
284
+ const assetLayer = p.type === 'knowledge' ? '07-Knowledge' :
285
+ p.type === 'insight' ? '08-Insights' :
286
+ p.type === 'lesson' ? '12-Analysis' : null;
287
+ if (assetLayer) {
288
+ try {
289
+ const { writeContextAsset } = await import('./context-os.js');
290
+ await writeContextAsset({
291
+ layer: assetLayer,
292
+ title: p.content.slice(0, 40),
293
+ content: `> 来源: session 价值点自动路由 (${p.type})\n\n${p.content}\n\n## 价值判断自检\n未来哪个具体场景会用到它? (待确认后固化, 当前 stage0)`,
294
+ tags: [p.type, 'auto-routed'],
295
+ domain: opts.channelId,
296
+ }, opts.home);
297
+ }
298
+ catch { /* 资产层写入失败不影响 */ }
299
+ }
300
+ }
301
+ catch {
302
+ /* 单条失败跳过 */
303
+ }
304
+ }
305
+ return written;
306
+ }
@@ -4,6 +4,12 @@
4
4
  * 失败静默: 文件不存在 → 字段 = '', 不抛错
5
5
  * 安全: agentId sanitize (防路径穿越)
6
6
  * 6 段输出顺序: identity → soul → project → user → agent → wiki
7
+ *
8
+ * 2026-08-03 (Context OS 融合 P1):
9
+ * - 支持 persona 文件 frontmatter 里的判断力声明 (judgment_style / stakes_default / revisable),
10
+ * 与 judgeness 5 维 facets 对应 — persona 提供"这个人怎么判断"的入口.
11
+ * - formatPersonaForSystemPrompt 固定追加 INJECT 工作纪律段 (Context OS 读取协议),
12
+ * 任何 channel 即使无 persona 文件也有纪律约束.
7
13
  */
8
14
  import * as fs from 'fs/promises';
9
15
  import * as os from 'os';
@@ -45,6 +51,71 @@ export async function loadPersonaDocs(agentId, home) {
45
51
  }));
46
52
  return docs;
47
53
  }
54
+ /**
55
+ * 轻量 frontmatter 解析 (不依赖 js-yaml).
56
+ * 只认 `key: value` 单行字段; 无 frontmatter (不以 --- 开头) → 返回 {}.
57
+ */
58
+ export function parseSimpleFrontmatter(content) {
59
+ const out = {};
60
+ const m = content.match(/^---\s*\r?\n([\s\S]*?)\r?\n---/);
61
+ if (!m)
62
+ return out;
63
+ for (const line of m[1].split('\n')) {
64
+ const kv = line.match(/^\s*([A-Za-z0-9_-]+)\s*:\s*(.*?)\s*$/);
65
+ if (kv)
66
+ out[kv[1]] = kv[2].replace(/^['"]|['"]$/g, '').trim();
67
+ }
68
+ return out;
69
+ }
70
+ /**
71
+ * 读 persona 6 文件 frontmatter 里的判断力声明 (Context OS 入口层 ↔ judgeness 5 维).
72
+ * 聚合所有文件中的 judgment_style / stakes_default / revisable 字段 (后者优先).
73
+ * 失败静默: 无 persona 文件 → 全部空值, 不抛错.
74
+ */
75
+ export async function loadPersonaJudgmentDeclaration(agentId, home) {
76
+ const safeId = sanitizeAgentId(agentId);
77
+ const root = home || os.homedir();
78
+ const baseDir = path.join(root, '.bolloon', 'persona', safeId);
79
+ const decl = {
80
+ judgmentStyle: '',
81
+ stakesDefault: '',
82
+ revisable: true,
83
+ raw: {},
84
+ };
85
+ await Promise.all(FILE_KEYS.map(async (key) => {
86
+ try {
87
+ const content = await fs.readFile(path.join(baseDir, `${key}.md`), 'utf-8');
88
+ const fm = parseSimpleFrontmatter(content);
89
+ for (const [k, v] of Object.entries(fm)) {
90
+ if (k.startsWith('judgment_') || k === 'stakes_default' || k === 'revisable') {
91
+ decl.raw[k] = v;
92
+ }
93
+ }
94
+ }
95
+ catch {
96
+ /* 单文件缺失/损坏跳过 */
97
+ }
98
+ }));
99
+ decl.judgmentStyle = decl.raw['judgment_style'] || decl.raw['judgmentStyle'] || '';
100
+ decl.stakesDefault = decl.raw['stakes_default'] || decl.raw['stakesDefault'] || '';
101
+ decl.revisable = decl.raw['revisable'] !== 'false';
102
+ return decl;
103
+ }
104
+ /** 把判断力声明格式化成一行段 (server contextHint 注入用) */
105
+ export function formatJudgmentDeclaration(decl) {
106
+ if (!decl.judgmentStyle && !decl.stakesDefault && !decl.raw['revisable'])
107
+ return '';
108
+ const parts = [];
109
+ if (decl.judgmentStyle)
110
+ parts.push(`风格: ${decl.judgmentStyle}`);
111
+ if (decl.stakesDefault)
112
+ parts.push(`默认风险等级: ${decl.stakesDefault}`);
113
+ if (decl.raw['revisable'] === 'false')
114
+ parts.push('偏好不可回滚的决策 (谨慎)');
115
+ else if (decl.raw['revisable'] === 'true')
116
+ parts.push('偏好可回滚的决策');
117
+ return `[系统上下文] 判断风格声明 (来自 persona frontmatter, 与 judgeness 判断资产对应):\n ${parts.join(' / ')}\n\n`;
118
+ }
48
119
  const DEFAULT_MAX_CHARS = 4000;
49
120
  const SECTION_LABELS = {
50
121
  identity: 'Identity',
@@ -60,6 +131,9 @@ const OUTPUT_ORDER = ['identity', 'soul', 'project', 'user', 'agent', 'wiki'];
60
131
  *
61
132
  * 超 maxChars 时按比例截断: 每个字段都保留头部,
62
133
  * 保证 6 段标识都出现, 不砍段.
134
+ *
135
+ * 2026-08-03: 固定追加 INJECT 工作纪律段 (Context OS 读取协议 §4/§10).
136
+ * 纪律段不参与动态预算 — 即使没有 persona 文件也有纪律约束.
63
137
  */
64
138
  export function formatPersonaForSystemPrompt(docs, maxChars) {
65
139
  const cap = maxChars ?? DEFAULT_MAX_CHARS;
@@ -71,11 +145,20 @@ export function formatPersonaForSystemPrompt(docs, maxChars) {
71
145
  sections.push({ key, text: `## ${SECTION_LABELS[key]}\n${v}` });
72
146
  }
73
147
  }
74
- if (sections.length === 0)
75
- return '';
76
- // 预算每段 (去掉 ## 标识和换行的固定开销)
148
+ // INJECT 工作纪律 (Context OS §4 最小读取集 + §10 工作规则, 精简 4 条)
149
+ const discipline = '## 工作纪律 (INJECT)\n' +
150
+ '1. 先看动态状态 (历史记忆 / 进行中的计划), 再按任务路由读取对应文档; 不读到的内容不假装知道.\n' +
151
+ '2. 区分: 已知事实 / 你的判断 / 需要用户确认的内容.\n' +
152
+ '3. 重要决策前列出: 选项 (含不做)、成本、收益、风险、信息缺口、回滚条件 — 决策可追溯.\n' +
153
+ '4. 对话收尾提取可复用的决策/知识/教训, 归档到唯一位置, 不制造重复文件.';
77
154
  const header = `# Persona (agentId=${docs.agentId})\n\n`;
78
- const fixedOverhead = header.length + (sections.length - 1) * 2;
155
+ // persona 文件 只输出纪律段
156
+ if (sections.length === 0) {
157
+ const solo = `${header}${discipline}`;
158
+ return solo.length > cap ? solo.slice(0, cap) : solo;
159
+ }
160
+ // 预算每段 (去掉 ## 标识和换行的固定开销; 纪律段固定, 动态段让出预算)
161
+ const fixedOverhead = header.length + discipline.length + (sections.length - 1) * 2;
79
162
  const perSectionBudget = Math.max(50, Math.floor((cap - fixedOverhead) / sections.length));
80
163
  const parts = [header.trim()];
81
164
  for (const sec of sections) {
@@ -85,7 +168,7 @@ export function formatPersonaForSystemPrompt(docs, maxChars) {
85
168
  }
86
169
  parts.push(body);
87
170
  }
88
- let result = parts.join('\n\n');
171
+ let result = parts.join('\n\n') + '\n\n' + discipline;
89
172
  if (result.length > cap) {
90
173
  const truncateMarker = '\n... (截断)';
91
174
  result = result.substring(0, Math.max(0, cap - truncateMarker.length)) + truncateMarker;