@bolloon/bolloon-agent 0.3.27 → 0.3.29

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,247 @@
1
+ /**
2
+ * decision-store.ts — 决策协议 (Context OS §7, 2026-08-03)
3
+ *
4
+ * Context OS 原则: 重要决策不允许只写结论, 至少要留下可回滚的推理链.
5
+ * 9 要素:
6
+ * 1. 问题到底是什么 → problem
7
+ * 2. 有哪些选项, 包括"不做" → options[].label (includeDoNothing)
8
+ * 3. 每个选项的时间/金钱/精力/机会成本 → options[].costs
9
+ * 4. 每个选项的短期/长期收益和战略价值 → options[].benefits
10
+ * 5. 风险、概率、影响、是否可恢复 → options[].risks
11
+ * 6. 当前的信息缺口 → infoGaps
12
+ * 7. 推荐方案 → recommendation
13
+ * 8. 为什么是现在 → timing
14
+ * 9. 失败时的触发条件和回滚动作 → rollback
15
+ *
16
+ * 防止三类错误: 把一时情绪当判断 / 因一条新信息推翻整个项目 / 把不能回滚的重投入当"勇敢".
17
+ * 原则: 新信息只修改它真正证伪的那个假设; 其他部分默认不动.
18
+ *
19
+ * 设计 (减法, 同 plan-store):
20
+ * - 不建数据库. 落盘 = ~/.bolloon/decisions/<decisionId>.json
21
+ * - 工具入口在 pi-sdk-tools.ts 注册, 复用现有 LLM 调度.
22
+ * - 任何 IO 失败静默返回 error 对象, 不阻塞主对话.
23
+ * - 决策确认 (decideDecision) 后自动 reflect 到 judgeness:
24
+ * HumanJudgment (storeHumanJudgment, source=trajectory) + JudgenessDescription
25
+ * (reflectAfterJudgment, openState=locked → Context OS 阶段0 临时价值点).
26
+ */
27
+ import * as fs from 'fs/promises';
28
+ import * as os from 'os';
29
+ import * as path from 'path';
30
+ import * as crypto from 'crypto';
31
+ // ============================================================
32
+ // 落盘路径
33
+ // ============================================================
34
+ export function getDecisionsDir(home = os.homedir()) {
35
+ return path.join(home, '.bolloon', 'decisions');
36
+ }
37
+ export function getDecisionPath(decisionId, home = os.homedir()) {
38
+ return path.join(getDecisionsDir(home), `${decisionId.replace(/[^a-zA-Z0-9_-]/g, '_')}.json`);
39
+ }
40
+ export function sanitizeDecisionId(id) {
41
+ return id.replace(/[^a-zA-Z0-9_-]/g, '_').slice(0, 64);
42
+ }
43
+ export async function createDecision(input, home) {
44
+ const problem = String(input.problem || '').trim();
45
+ if (!problem)
46
+ return { ok: false, error: 'problem 必填 (问题到底是什么)' };
47
+ const options = Array.isArray(input.options)
48
+ ? input.options
49
+ .map((o) => ({
50
+ label: String(o.label || '').trim(),
51
+ includeDoNothing: !!o.includeDoNothing,
52
+ costs: String(o.costs || ''),
53
+ benefits: String(o.benefits || ''),
54
+ risks: String(o.risks || ''),
55
+ }))
56
+ .filter((o) => o.label)
57
+ : [];
58
+ if (options.length === 0)
59
+ return { ok: false, error: 'options 至少 1 个选项 (含"不做")' };
60
+ const now = new Date().toISOString();
61
+ const decisionId = `dec_${Date.now()}_${crypto.randomBytes(3).toString('hex')}`;
62
+ const decision = {
63
+ decisionId,
64
+ problem,
65
+ options,
66
+ infoGaps: String(input.infoGaps || ''),
67
+ recommendation: String(input.recommendation || ''),
68
+ timing: String(input.timing || ''),
69
+ rollback: String(input.rollback || ''),
70
+ context: {
71
+ domain: String(input.domain || '通用'),
72
+ stakes: input.stakes || 'medium',
73
+ by: input.by || 'agent',
74
+ originChannel: String(input.originChannel || ''),
75
+ },
76
+ status: 'draft',
77
+ createdAt: now,
78
+ updatedAt: now,
79
+ };
80
+ try {
81
+ const dir = getDecisionsDir(home);
82
+ await fs.mkdir(dir, { recursive: true });
83
+ await fs.writeFile(getDecisionPath(decisionId, home), JSON.stringify(decision, null, 2), 'utf-8');
84
+ return { ok: true, decision };
85
+ }
86
+ catch (e) {
87
+ return { ok: false, error: `写入失败: ${e?.message || String(e)}` };
88
+ }
89
+ }
90
+ export async function loadDecision(decisionId, home) {
91
+ try {
92
+ const raw = await fs.readFile(getDecisionPath(decisionId, home), 'utf-8');
93
+ return JSON.parse(raw);
94
+ }
95
+ catch {
96
+ return null;
97
+ }
98
+ }
99
+ async function saveDecision(decision, home) {
100
+ decision.updatedAt = new Date().toISOString();
101
+ await fs.writeFile(getDecisionPath(decision.decisionId, home), JSON.stringify(decision, null, 2), 'utf-8');
102
+ }
103
+ export async function updateDecisionStatus(decisionId, input, opts = {}) {
104
+ const decision = await loadDecision(decisionId, opts.home);
105
+ if (!decision)
106
+ return { ok: false, error: `decision '${decisionId}' 不存在` };
107
+ const now = new Date().toISOString();
108
+ if (input.decide) {
109
+ if (input.recommendation && input.recommendation.trim()) {
110
+ decision.recommendation = String(input.recommendation).trim();
111
+ }
112
+ if (!decision.recommendation)
113
+ return { ok: false, error: 'recommendation 必填 (推荐方案)' };
114
+ decision.status = 'decided';
115
+ decision.decidedAt = now;
116
+ // 决策确认 → 自动 reflect 到 judgeness (Context OS 阶段0 入账)
117
+ const refl = await reflectDecisionToJudgeness(decision, opts.byAgentId);
118
+ if (!refl.ok) {
119
+ return { ok: false, error: `决策确认失败 (judgeness 入库): ${refl.error}` };
120
+ }
121
+ decision.reflection = refl.reflection;
122
+ }
123
+ else if (input.implemented) {
124
+ if (decision.status !== 'decided' && decision.status !== 'implemented') {
125
+ return { ok: false, error: '只能把 decided 的决策标记为 implemented' };
126
+ }
127
+ decision.status = 'implemented';
128
+ }
129
+ else if (input.rollback || input.abandon) {
130
+ decision.status = input.rollback ? 'rolled-back' : 'abandoned';
131
+ // 回滚/放弃 → 教训入库 (reject 语义, 防重复踩坑)
132
+ const reason = String(input.reason || '');
133
+ if (reason && input.rollback) {
134
+ await reflectRollbackLesson(decision, reason, opts.byAgentId).catch(() => { });
135
+ }
136
+ }
137
+ else {
138
+ return { ok: false, error: '必须指定 decide / implemented / rollback / abandon 之一' };
139
+ }
140
+ await saveDecision(decision, opts.home);
141
+ return { ok: true, decision };
142
+ }
143
+ export async function listDecisions(status, home) {
144
+ try {
145
+ const dir = getDecisionsDir(home);
146
+ const files = await fs.readdir(dir);
147
+ const out = [];
148
+ for (const f of files) {
149
+ if (!f.endsWith('.json'))
150
+ continue;
151
+ try {
152
+ const raw = await fs.readFile(path.join(dir, f), 'utf-8');
153
+ const d = JSON.parse(raw);
154
+ if (status && d.status !== status)
155
+ continue;
156
+ out.push(d);
157
+ }
158
+ catch { /* 单文件损坏跳过 */ }
159
+ }
160
+ out.sort((a, b) => b.createdAt.localeCompare(a.createdAt));
161
+ return out;
162
+ }
163
+ catch {
164
+ return [];
165
+ }
166
+ }
167
+ /** 决策摘要 → 上下文注入 (9 要素完整可追溯, 上下文只给精简版) */
168
+ export function decisionToContext(d) {
169
+ const lines = [`📌 ${d.problem} [${d.status}] (id=${d.decisionId})`];
170
+ for (const o of d.options) {
171
+ const tag = o.includeDoNothing ? ' [不做]' : '';
172
+ lines.push(` - 选项${tag}: ${o.label}`);
173
+ }
174
+ if (d.recommendation)
175
+ lines.push(` 推荐: ${d.recommendation}`);
176
+ if (d.infoGaps)
177
+ lines.push(` 信息缺口: ${d.infoGaps}`);
178
+ if (d.rollback)
179
+ lines.push(` 回滚条件: ${d.rollback}`);
180
+ if (d.reflection)
181
+ lines.push(` ✅ 已入库 judgeness: hv=${d.reflection.hvId}${d.reflection.jdId ? ` jd=${d.reflection.jdId}` : ''}`);
182
+ return lines.join('\n');
183
+ }
184
+ /** 决策确认后入库: HumanJudgment (source=trajectory) + reflectAfterJudgment (openState=locked) */
185
+ export async function reflectDecisionToJudgeness(decision, byAgentId) {
186
+ try {
187
+ const { storeHumanJudgment } = await import('../pi-ecosystem-judgment/human-value-store.js');
188
+ const { reflectAfterJudgment } = await import('../judgeness/reflect.js');
189
+ const decisionText = `${decision.recommendation} (问题: ${decision.problem})`;
190
+ const judgment = await storeHumanJudgment({
191
+ decision: decisionText.slice(0, 300),
192
+ decision_type: 'approve',
193
+ reasons: [
194
+ `选项: ${decision.options.map((o) => o.label).join(' / ')}`,
195
+ `风险: ${decision.options.map((o) => o.risks || '无').join('; ')}`,
196
+ `回滚: ${decision.rollback || '无'}`,
197
+ ].filter(Boolean),
198
+ values_derived: [],
199
+ context: {
200
+ domain: decision.context.domain,
201
+ complexity: decision.context.stakes === 'critical' ? 'profound' : decision.context.stakes === 'high' ? 'complex' : decision.context.stakes === 'medium' ? 'moderate' : 'simple',
202
+ stakes: decision.context.stakes,
203
+ time_pressure: 'medium',
204
+ },
205
+ outcome: { approved: true },
206
+ metadata: { source: 'trajectory', confidence: 0.8, revisable: true },
207
+ status: 'active',
208
+ appliesTo: [],
209
+ });
210
+ const jd = await reflectAfterJudgment(judgment, 'agent', byAgentId).catch(() => null);
211
+ return {
212
+ ok: true,
213
+ reflection: { hvId: judgment.id, jdId: jd?.descriptionId, at: new Date().toISOString() },
214
+ };
215
+ }
216
+ catch (e) {
217
+ return { ok: false, error: e?.message || String(e) };
218
+ }
219
+ }
220
+ /** 回滚教训入库: HumanJudgment (reject 语义) — Context OS "失败时的触发条件和回滚动作" */
221
+ async function reflectRollbackLesson(decision, reason, byAgentId) {
222
+ try {
223
+ const { storeHumanJudgment } = await import('../pi-ecosystem-judgment/human-value-store.js');
224
+ const { reflectAfterJudgment } = await import('../judgeness/reflect.js');
225
+ const judgment = await storeHumanJudgment({
226
+ decision: `回滚决策: ${decision.problem} — 失败原因: ${reason.slice(0, 200)}`,
227
+ decision_type: 'reject',
228
+ reasons: [decision.rollback || '', reason].filter(Boolean),
229
+ values_derived: [],
230
+ context: {
231
+ domain: decision.context.domain,
232
+ complexity: 'moderate',
233
+ stakes: decision.context.stakes,
234
+ time_pressure: 'medium',
235
+ },
236
+ outcome: { approved: false, feedback: reason.slice(0, 200) },
237
+ metadata: { source: 'trajectory', confidence: 0.7, revisable: false },
238
+ status: 'active',
239
+ appliesTo: [],
240
+ });
241
+ const jd = await reflectAfterJudgment(judgment, 'agent', byAgentId).catch(() => null);
242
+ return { ok: true, reflection: { hvId: judgment.id, jdId: jd?.descriptionId, at: new Date().toISOString() } };
243
+ }
244
+ catch (e) {
245
+ return { ok: false, error: e?.message || String(e) };
246
+ }
247
+ }