@shadowforge0/aquifer-memory 1.5.12 → 1.7.0
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/.env.example +23 -0
- package/README.md +84 -73
- package/README_CN.md +676 -0
- package/README_TW.md +684 -0
- package/aquifer.config.example.json +34 -0
- package/consumers/claude-code.js +11 -11
- package/consumers/cli.js +421 -53
- package/consumers/codex-handoff.js +258 -0
- package/consumers/codex.js +1676 -0
- package/consumers/default/daily-entries.js +23 -4
- package/consumers/default/index.js +2 -2
- package/consumers/default/prompts/summary.js +6 -6
- package/consumers/mcp.js +96 -5
- package/consumers/openclaw-ext/index.js +0 -1
- package/consumers/openclaw-plugin.js +1 -1
- package/consumers/shared/config.js +8 -0
- package/consumers/shared/factory.js +1 -0
- package/consumers/shared/ingest.js +1 -1
- package/consumers/shared/normalize.js +14 -3
- package/consumers/shared/recall-format.js +27 -0
- package/consumers/shared/summary-parser.js +151 -0
- package/core/aquifer.js +380 -18
- package/core/finalization-review.js +319 -0
- package/core/mcp-manifest.js +52 -2
- package/core/memory-bootstrap.js +200 -0
- package/core/memory-consolidation.js +1590 -0
- package/core/memory-promotion.js +544 -0
- package/core/memory-recall.js +247 -0
- package/core/memory-records.js +797 -0
- package/core/memory-safety-gate.js +224 -0
- package/core/session-finalization.js +365 -0
- package/core/storage.js +385 -2
- package/docs/getting-started.md +105 -0
- package/docs/postprocess-contract.md +2 -2
- package/docs/setup.md +92 -2
- package/package.json +25 -11
- package/pipeline/normalize/adapters/codex.js +106 -0
- package/pipeline/normalize/detect.js +3 -2
- package/schema/001-base.sql +3 -0
- package/schema/007-v1-foundation.sql +273 -0
- package/schema/008-session-finalizations.sql +50 -0
- package/schema/009-v1-assertion-plane.sql +193 -0
- package/schema/010-v1-finalization-review.sql +160 -0
- package/schema/011-v1-compaction-claim.sql +46 -0
- package/schema/012-v1-compaction-lease.sql +39 -0
- package/schema/013-v1-compaction-lineage.sql +193 -0
- package/scripts/codex-recovery.js +672 -0
- package/consumers/miranda/context-inject.js +0 -120
- package/consumers/miranda/daily-entries.js +0 -224
- package/consumers/miranda/index.js +0 -364
- package/consumers/miranda/instance.js +0 -55
- package/consumers/miranda/llm.js +0 -99
- package/consumers/miranda/profile.json +0 -145
- package/consumers/miranda/prompts/summary.js +0 -303
- package/consumers/miranda/recall-format.js +0 -76
- package/consumers/miranda/render-daily-md.js +0 -186
- package/consumers/miranda/workspace-files.js +0 -91
- package/scripts/drop-entity-state-history.sql +0 -17
- package/scripts/drop-insights.sql +0 -12
- package/scripts/install-openclaw.sh +0 -59
|
@@ -1,364 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
// ---------------------------------------------------------------------------
|
|
4
|
-
// Miranda persona layer.
|
|
5
|
-
//
|
|
6
|
-
// This is a PERSONA, not a host adapter. It wraps a host (OpenClaw gateway,
|
|
7
|
-
// Claude Code afterburn) with Miranda's six-section prompt, zh-TW daily log,
|
|
8
|
-
// workspace file artifacts, and consolidation lifecycle — but leaves the
|
|
9
|
-
// actual hook plumbing (before_reset, before_prompt_build, MCP tool registry)
|
|
10
|
-
// to the host.
|
|
11
|
-
//
|
|
12
|
-
// Entry points:
|
|
13
|
-
// mountOnOpenClaw(api, opts) — gateway plugin wiring
|
|
14
|
-
// mountOnClaudeCode(cc, opts) — CC afterburn wiring (see consumers/claude-code.js)
|
|
15
|
-
// buildPostProcess(opts) — low-level: returns the enrich postProcess
|
|
16
|
-
// fn; host calls it directly if neither
|
|
17
|
-
// mount helper fits.
|
|
18
|
-
// ---------------------------------------------------------------------------
|
|
19
|
-
|
|
20
|
-
const { runIngest } = require('../shared/ingest');
|
|
21
|
-
const { parseEntitySection } = require('../shared/entity-parser');
|
|
22
|
-
|
|
23
|
-
const instance = require('./instance');
|
|
24
|
-
const { callLlm, resolveModel, loadConfig } = require('./llm');
|
|
25
|
-
const summary = require('./prompts/summary');
|
|
26
|
-
const dailyEntries = require('./daily-entries');
|
|
27
|
-
const workspaceFiles = require('./workspace-files');
|
|
28
|
-
const contextInject = require('./context-inject');
|
|
29
|
-
const mirandaRecallFormat = require('./recall-format');
|
|
30
|
-
|
|
31
|
-
// ---------------------------------------------------------------------------
|
|
32
|
-
// summaryFn / entityParseFn factories — shared by gateway + CC
|
|
33
|
-
// ---------------------------------------------------------------------------
|
|
34
|
-
|
|
35
|
-
function buildSummaryFn({ agentId, now, dailyContext, runtime = 'gateway', logger = console }) {
|
|
36
|
-
return async function summaryFn(_normalized) {
|
|
37
|
-
// _normalized is the cleaned messages from Aquifer; Miranda wants the
|
|
38
|
-
// reconstructed conversation text for the six-section prompt.
|
|
39
|
-
const conversationText = extractConversationText(_normalized);
|
|
40
|
-
if (!conversationText) throw new Error('empty conversation text');
|
|
41
|
-
|
|
42
|
-
const prompt = summary.buildSummaryPrompt({ conversationText, agentId, now, dailyContext });
|
|
43
|
-
if (logger.info) logger.info(`[miranda] calling LLM (${runtime})`);
|
|
44
|
-
const output = await callLlm(prompt, { runtime });
|
|
45
|
-
if (!output) throw new Error('LLM returned empty');
|
|
46
|
-
|
|
47
|
-
const sections = summary.parseSummaryOutput(output);
|
|
48
|
-
const recap = summary.parseRecapLines(sections.recap || '');
|
|
49
|
-
const workingFacts = summary.parseWorkingFacts(sections.working_facts || '');
|
|
50
|
-
if (!recap.title) throw new Error('LLM recap missing title');
|
|
51
|
-
|
|
52
|
-
return {
|
|
53
|
-
summaryText: recap.overview || '',
|
|
54
|
-
structuredSummary: { ...recap, raw_sections: sections },
|
|
55
|
-
entityRaw: sections.entities || null,
|
|
56
|
-
extra: { sections, recap, workingFacts },
|
|
57
|
-
};
|
|
58
|
-
};
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function buildEntityParseFn() {
|
|
62
|
-
return function entityParseFn(text) {
|
|
63
|
-
const parsed = parseEntitySection(text);
|
|
64
|
-
return parsed.entities; // already has { name, normalizedName, aliases, type }
|
|
65
|
-
};
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
function extractConversationText(normalized) {
|
|
69
|
-
if (!Array.isArray(normalized)) return '';
|
|
70
|
-
return normalized
|
|
71
|
-
.filter(m => m.role === 'user' || m.role === 'assistant')
|
|
72
|
-
.map(m => `[${m.role}] ${typeof m.content === 'string' ? m.content : ''}`)
|
|
73
|
-
.join('\n');
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
// ---------------------------------------------------------------------------
|
|
77
|
-
// buildPostProcess — produce the enrich postProcess hook for Miranda
|
|
78
|
-
// ---------------------------------------------------------------------------
|
|
79
|
-
|
|
80
|
-
/**
|
|
81
|
-
* @param {object} opts
|
|
82
|
-
* @param {object} opts.aquifer — Aquifer instance
|
|
83
|
-
* @param {object} opts.pool — pg.Pool (used for daily-entries DAL)
|
|
84
|
-
* @param {string} opts.agentId
|
|
85
|
-
* @param {string} [opts.workspaceDir] — if set, writes emotional-state.md and recap JSON files
|
|
86
|
-
* @param {string} [opts.source='afterburn']
|
|
87
|
-
* @param {string|null} [opts.tag=null] — daily-entry tag (e.g. '[CLI]' for CC runs)
|
|
88
|
-
* @param {Date} [opts.now]
|
|
89
|
-
* @param {object} [opts.logger]
|
|
90
|
-
* @param {boolean} [opts.consolidate=true]
|
|
91
|
-
*/
|
|
92
|
-
function buildPostProcess({
|
|
93
|
-
aquifer, pool, agentId, workspaceDir = null,
|
|
94
|
-
source = 'afterburn', tag = null, now = null,
|
|
95
|
-
logger = console, consolidate = true,
|
|
96
|
-
} = {}) {
|
|
97
|
-
if (!aquifer) throw new Error('buildPostProcess: aquifer is required');
|
|
98
|
-
if (!pool) throw new Error('buildPostProcess: pool is required');
|
|
99
|
-
if (!agentId) throw new Error('buildPostProcess: agentId is required');
|
|
100
|
-
|
|
101
|
-
return async function postProcess(ctx) {
|
|
102
|
-
const _now = now || new Date();
|
|
103
|
-
const recap = ctx.extra?.recap || null;
|
|
104
|
-
const sections = ctx.extra?.sections || null;
|
|
105
|
-
const workingFacts = ctx.extra?.workingFacts || [];
|
|
106
|
-
const sessionId = ctx.session.sessionId;
|
|
107
|
-
|
|
108
|
-
// 1. Workspace files (optional — only if persona has a workspace dir)
|
|
109
|
-
if (workspaceDir && (sections || recap)) {
|
|
110
|
-
try {
|
|
111
|
-
await workspaceFiles.writeWorkspaceFiles(sections || {}, recap, workspaceDir, {
|
|
112
|
-
sessionId,
|
|
113
|
-
agentId,
|
|
114
|
-
conversationText: extractConversationText(ctx.normalized || []),
|
|
115
|
-
}, logger);
|
|
116
|
-
} catch (err) {
|
|
117
|
-
if (logger.warn) logger.warn(`[miranda] workspace files failed: ${err.message}`);
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
// 2. Daily entries
|
|
122
|
-
if (sections || recap) {
|
|
123
|
-
try {
|
|
124
|
-
await dailyEntries.writeDailyEntries({
|
|
125
|
-
sections: sections || {}, recap, pool, sessionId, agentId, logger,
|
|
126
|
-
source, tag, now: _now,
|
|
127
|
-
});
|
|
128
|
-
} catch (err) {
|
|
129
|
-
if (logger.warn) logger.warn(`[miranda] daily entries failed: ${err.message}`);
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
// 3. Fact candidates — write to aquifer.${schema}.facts via consolidate 'create'
|
|
134
|
-
// (We only CREATE here; candidate-lifecycle decisions come from the
|
|
135
|
-
// consolidation step below.)
|
|
136
|
-
if (consolidate && workingFacts.length > 0) {
|
|
137
|
-
try {
|
|
138
|
-
const { normalizeEntityName } = require('../../index');
|
|
139
|
-
const actions = workingFacts.map(f => ({
|
|
140
|
-
action: 'create',
|
|
141
|
-
subject: f.subject,
|
|
142
|
-
statement: f.statement,
|
|
143
|
-
importance: 6,
|
|
144
|
-
}));
|
|
145
|
-
await aquifer.consolidate(sessionId, {
|
|
146
|
-
agentId,
|
|
147
|
-
actions,
|
|
148
|
-
normalizeSubject: normalizeEntityName,
|
|
149
|
-
recapOverview: recap?.overview || '',
|
|
150
|
-
});
|
|
151
|
-
} catch (err) {
|
|
152
|
-
if (logger.warn) logger.warn(`[miranda] fact candidates failed: ${err.message}`);
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
};
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
// ---------------------------------------------------------------------------
|
|
159
|
-
// Mount helpers — afterburn / context-inject / session_recall tool.
|
|
160
|
-
// Each is independently mountable so different OpenClaw extensions can claim
|
|
161
|
-
// the hooks they care about. mountOnOpenClaw() is the all-in-one convenience.
|
|
162
|
-
// ---------------------------------------------------------------------------
|
|
163
|
-
|
|
164
|
-
function resolveCommon(opts) {
|
|
165
|
-
if (!opts.pool) throw new Error('Miranda: pool is required');
|
|
166
|
-
if (!opts.embedFn) throw new Error('Miranda: embedFn is required');
|
|
167
|
-
return {
|
|
168
|
-
pool: opts.pool,
|
|
169
|
-
embedFn: opts.embedFn,
|
|
170
|
-
defaultAgentId: opts.agentId || 'main',
|
|
171
|
-
workspaceDir: opts.workspaceDir || null,
|
|
172
|
-
minUserMessages: opts.minUserMessages || 3,
|
|
173
|
-
aquifer: instance.getAquifer({
|
|
174
|
-
pool: opts.pool, embedFn: opts.embedFn, llmFn: opts.llmFn, rerankKey: opts.rerankKey,
|
|
175
|
-
}),
|
|
176
|
-
};
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
/**
|
|
180
|
-
* Register Miranda's afterburn hook (before_reset) on the OpenClaw api.
|
|
181
|
-
* Runs commit + enrich with Miranda's summaryFn and postProcess.
|
|
182
|
-
*/
|
|
183
|
-
function registerAfterburn(api, opts = {}) {
|
|
184
|
-
const { pool, aquifer, defaultAgentId, workspaceDir, minUserMessages } = resolveCommon(opts);
|
|
185
|
-
const recentlyProcessed = new Map();
|
|
186
|
-
const inFlight = new Set();
|
|
187
|
-
|
|
188
|
-
api.on('before_reset', (event, ctx) => {
|
|
189
|
-
const sessionId = ctx?.sessionId || event?.sessionId;
|
|
190
|
-
const agentId = ctx?.agentId || defaultAgentId;
|
|
191
|
-
const sessionKey = ctx?.sessionKey || null;
|
|
192
|
-
|
|
193
|
-
if (!sessionId) return;
|
|
194
|
-
if ((sessionKey || '').includes('subagent')) return;
|
|
195
|
-
if ((sessionKey || '').includes(':cron:')) return;
|
|
196
|
-
|
|
197
|
-
const rawEntries = Array.isArray(event?.messages) ? event.messages : [];
|
|
198
|
-
if (rawEntries.length < 3) {
|
|
199
|
-
api.logger.info(`[miranda] skip ${sessionId}: only ${rawEntries.length} msgs`);
|
|
200
|
-
return;
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
(async () => {
|
|
204
|
-
try {
|
|
205
|
-
const now = new Date();
|
|
206
|
-
const date = dailyEntries.taipeiDateString(now);
|
|
207
|
-
let dailyContext = '';
|
|
208
|
-
try { dailyContext = await dailyEntries.fetchDailyContext(pool, date, agentId); } catch { /* best-effort */ }
|
|
209
|
-
|
|
210
|
-
await runIngest({
|
|
211
|
-
aquifer,
|
|
212
|
-
sessionId,
|
|
213
|
-
agentId,
|
|
214
|
-
source: 'openclaw',
|
|
215
|
-
sessionKey,
|
|
216
|
-
adapter: 'gateway',
|
|
217
|
-
rawEntries,
|
|
218
|
-
minUserMessages,
|
|
219
|
-
dedupMap: recentlyProcessed,
|
|
220
|
-
inFlight,
|
|
221
|
-
summaryFn: buildSummaryFn({ agentId, now, dailyContext, runtime: 'gateway', logger: api.logger }),
|
|
222
|
-
entityParseFn: buildEntityParseFn(),
|
|
223
|
-
postProcess: buildPostProcess({
|
|
224
|
-
aquifer, pool, agentId, workspaceDir,
|
|
225
|
-
source: 'afterburn', now, logger: api.logger,
|
|
226
|
-
}),
|
|
227
|
-
logger: api.logger,
|
|
228
|
-
});
|
|
229
|
-
} catch (err) {
|
|
230
|
-
api.logger.warn(`[miranda] capture failed ${sessionId}: ${err.message}`);
|
|
231
|
-
}
|
|
232
|
-
})();
|
|
233
|
-
});
|
|
234
|
-
|
|
235
|
-
api.logger.info('[miranda] registerAfterburn: before_reset hooked');
|
|
236
|
-
return { aquifer };
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
/**
|
|
240
|
-
* Register the before_prompt_build Miranda context injection.
|
|
241
|
-
* Separate from afterburn so the Driftwood ext can install it while the
|
|
242
|
-
* afterburn ext owns the before_reset hook.
|
|
243
|
-
*/
|
|
244
|
-
function registerContextInject(api, opts = {}) {
|
|
245
|
-
const { pool, aquifer, defaultAgentId } = resolveCommon(opts);
|
|
246
|
-
|
|
247
|
-
api.on('before_prompt_build', async (event, ctx) => {
|
|
248
|
-
try {
|
|
249
|
-
const agentId = ctx?.agentId || defaultAgentId;
|
|
250
|
-
if ((ctx?.sessionKey || '').includes('subagent')) return;
|
|
251
|
-
|
|
252
|
-
const context = await contextInject.computeInjection({
|
|
253
|
-
aquifer, pool, agentId, includeBootstrap: true,
|
|
254
|
-
});
|
|
255
|
-
if (context && context.split('\n').length > 3) {
|
|
256
|
-
api.logger.info(`[miranda] injecting context: ${context.length} chars, agent=${agentId}`);
|
|
257
|
-
return { prependSystemContext: context };
|
|
258
|
-
}
|
|
259
|
-
} catch (err) {
|
|
260
|
-
api.logger.warn(`[miranda] context injection failed: ${err.message}`);
|
|
261
|
-
}
|
|
262
|
-
});
|
|
263
|
-
|
|
264
|
-
api.logger.info('[miranda] registerContextInject: before_prompt_build hooked');
|
|
265
|
-
return { aquifer };
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
/**
|
|
269
|
-
* Register the zh-TW session_recall tool.
|
|
270
|
-
*/
|
|
271
|
-
function registerRecallTool(api, opts = {}) {
|
|
272
|
-
const { aquifer } = resolveCommon(opts);
|
|
273
|
-
|
|
274
|
-
api.registerTool((ctx) => {
|
|
275
|
-
if ((ctx?.sessionKey || '').includes('subagent')) return null;
|
|
276
|
-
return {
|
|
277
|
-
name: 'session_recall',
|
|
278
|
-
description: '搜尋歷史 session 的摘要和對話記錄。當問題明確提到具體人名、專案、工具、檔名時,傳 entities;只想保留全部命中的 session 用 entity_mode="all",否則 "any" 是 boost。mode 可選 "fts"/"vector"/"hybrid",default hybrid。',
|
|
279
|
-
parameters: {
|
|
280
|
-
type: 'object',
|
|
281
|
-
properties: {
|
|
282
|
-
query: { type: 'string', description: '搜尋關鍵字或自然語言描述(必填非空)', minLength: 1 },
|
|
283
|
-
date_from: { type: 'string' }, date_to: { type: 'string' },
|
|
284
|
-
agent_id: { type: 'string' }, source: { type: 'string' },
|
|
285
|
-
entities: { type: 'array', items: { type: 'string' }, description: '具名 entity 清單(人/專案/工具/檔名)' },
|
|
286
|
-
entity_mode: { type: 'string', enum: ['any', 'all'], description: '"any" boost / "all" 硬過濾必含全部 entity' },
|
|
287
|
-
mode: { type: 'string', enum: ['fts', 'hybrid', 'vector'], description: 'recall 模式,default hybrid' },
|
|
288
|
-
detail: { type: 'string' },
|
|
289
|
-
limit: { type: 'number' },
|
|
290
|
-
},
|
|
291
|
-
},
|
|
292
|
-
async execute(_toolCallId, params) {
|
|
293
|
-
try {
|
|
294
|
-
const limit = Math.max(1, Math.min(20, parseInt(params?.limit ?? 5, 10) || 5));
|
|
295
|
-
const recallOpts = {
|
|
296
|
-
agentId: params?.agent_id || ctx?.agentId || undefined,
|
|
297
|
-
source: params?.source || undefined,
|
|
298
|
-
dateFrom: params?.date_from || undefined,
|
|
299
|
-
dateTo: params?.date_to || undefined,
|
|
300
|
-
limit,
|
|
301
|
-
};
|
|
302
|
-
if (Array.isArray(params?.entities) && params.entities.length > 0) {
|
|
303
|
-
recallOpts.entities = params.entities;
|
|
304
|
-
recallOpts.entityMode = params?.entity_mode || 'any';
|
|
305
|
-
}
|
|
306
|
-
if (params?.mode === 'fts' || params?.mode === 'hybrid' || params?.mode === 'vector') {
|
|
307
|
-
recallOpts.mode = params.mode;
|
|
308
|
-
}
|
|
309
|
-
const results = await aquifer.recall(String(params?.query || ''), recallOpts);
|
|
310
|
-
const text = mirandaRecallFormat.formatRecallResults(results.map(r => ({
|
|
311
|
-
sessionId: r.sessionId, agentId: r.agentId, source: r.source,
|
|
312
|
-
startedAt: r.startedAt, summaryText: r.summaryText,
|
|
313
|
-
structuredSummary: r.structuredSummary,
|
|
314
|
-
matchedTurnText: r.matchedTurnText,
|
|
315
|
-
})));
|
|
316
|
-
return { content: [{ type: 'text', text }] };
|
|
317
|
-
} catch (err) {
|
|
318
|
-
return { content: [{ type: 'text', text: `session_recall 錯誤:${err.message}` }], isError: true };
|
|
319
|
-
}
|
|
320
|
-
},
|
|
321
|
-
};
|
|
322
|
-
}, { name: 'session_recall' });
|
|
323
|
-
|
|
324
|
-
api.logger.info('[miranda] registerRecallTool: session_recall tool registered');
|
|
325
|
-
return { aquifer };
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
/**
|
|
329
|
-
* Convenience: register all three on the same api. Equivalent to calling
|
|
330
|
-
* registerAfterburn + registerContextInject + registerRecallTool.
|
|
331
|
-
*/
|
|
332
|
-
function mountOnOpenClaw(api, opts = {}) {
|
|
333
|
-
const r1 = registerAfterburn(api, opts);
|
|
334
|
-
registerContextInject(api, opts);
|
|
335
|
-
registerRecallTool(api, opts);
|
|
336
|
-
return r1;
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
// ---------------------------------------------------------------------------
|
|
340
|
-
// Exports
|
|
341
|
-
// ---------------------------------------------------------------------------
|
|
342
|
-
|
|
343
|
-
module.exports = {
|
|
344
|
-
// Persona entry points
|
|
345
|
-
mountOnOpenClaw,
|
|
346
|
-
registerAfterburn,
|
|
347
|
-
registerContextInject,
|
|
348
|
-
registerRecallTool,
|
|
349
|
-
buildPostProcess,
|
|
350
|
-
buildSummaryFn,
|
|
351
|
-
buildEntityParseFn,
|
|
352
|
-
|
|
353
|
-
// Individual modules for advanced wiring
|
|
354
|
-
instance,
|
|
355
|
-
llm: { callLlm, resolveModel, loadConfig },
|
|
356
|
-
summary,
|
|
357
|
-
dailyEntries,
|
|
358
|
-
workspaceFiles,
|
|
359
|
-
contextInject,
|
|
360
|
-
recallFormat: mirandaRecallFormat,
|
|
361
|
-
|
|
362
|
-
// Helpers re-exported for convenience
|
|
363
|
-
extractConversationText,
|
|
364
|
-
};
|
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
// Miranda Aquifer instance factory — produces a singleton bound to the
|
|
4
|
-
// miranda schema + entity scope + rerank config. Host supplies the pg.Pool
|
|
5
|
-
// and embed function (they're host-specific wiring: OpenClaw has its own
|
|
6
|
-
// pg + embed libs; CC uses the same).
|
|
7
|
-
|
|
8
|
-
const { createAquifer } = require('../../index');
|
|
9
|
-
const { callLlm } = require('./llm');
|
|
10
|
-
|
|
11
|
-
let _instance = null;
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* @param {object} opts
|
|
15
|
-
* @param {object} opts.pool — pg.Pool from the host
|
|
16
|
-
* @param {function} opts.embedFn — async (texts: string[]) => number[][]
|
|
17
|
-
* @param {function} [opts.llmFn] — defaults to Miranda's MiniMax wrapper
|
|
18
|
-
* @param {string} [opts.rerankKey] — OpenRouter API key; falls back to
|
|
19
|
-
* process.env.OPENROUTER_API_KEY / AQUIFER_RERANK_API_KEY
|
|
20
|
-
* @returns {object} Aquifer instance
|
|
21
|
-
*/
|
|
22
|
-
function getAquifer(opts = {}) {
|
|
23
|
-
if (_instance) return _instance;
|
|
24
|
-
if (!opts.pool) throw new Error('Miranda: pool is required');
|
|
25
|
-
if (!opts.embedFn) throw new Error('Miranda: embedFn is required');
|
|
26
|
-
|
|
27
|
-
const rerankKey = opts.rerankKey
|
|
28
|
-
|| process.env.OPENROUTER_API_KEY
|
|
29
|
-
|| process.env.AQUIFER_RERANK_API_KEY;
|
|
30
|
-
|
|
31
|
-
_instance = createAquifer({
|
|
32
|
-
schema: 'miranda',
|
|
33
|
-
db: opts.pool,
|
|
34
|
-
tenantId: 'default',
|
|
35
|
-
embed: { fn: opts.embedFn },
|
|
36
|
-
llm: { fn: opts.llmFn || callLlm },
|
|
37
|
-
entities: { enabled: true, mergeCall: true, scope: 'miranda' },
|
|
38
|
-
facts: { enabled: true },
|
|
39
|
-
rerank: rerankKey ? {
|
|
40
|
-
provider: 'openrouter',
|
|
41
|
-
openrouterApiKey: rerankKey,
|
|
42
|
-
model: 'cohere/rerank-v3.5',
|
|
43
|
-
topK: 20,
|
|
44
|
-
maxChars: 1600,
|
|
45
|
-
timeout: 5000,
|
|
46
|
-
maxRetries: 1,
|
|
47
|
-
} : null,
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
return _instance;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
function resetAquifer() { _instance = null; }
|
|
54
|
-
|
|
55
|
-
module.exports = { getAquifer, resetAquifer };
|
package/consumers/miranda/llm.js
DELETED
|
@@ -1,99 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const fs = require('fs');
|
|
4
|
-
const path = require('path');
|
|
5
|
-
const os = require('os');
|
|
6
|
-
|
|
7
|
-
// Default host home. Callers can override via opts.envPath / opts.configDir.
|
|
8
|
-
const DEFAULT_HOME = process.env.OPENCLAW_HOME || path.join(os.homedir(), '.openclaw');
|
|
9
|
-
|
|
10
|
-
function loadEnvFile(envPath) {
|
|
11
|
-
if (!envPath) envPath = path.join(DEFAULT_HOME, '.env');
|
|
12
|
-
try {
|
|
13
|
-
const text = fs.readFileSync(envPath, 'utf8');
|
|
14
|
-
for (const line of text.split('\n')) {
|
|
15
|
-
const m = line.match(/^([A-Z_][A-Z0-9_]*)=(.*)$/);
|
|
16
|
-
if (m && !process.env[m[1]]) process.env[m[1]] = m[2].trim();
|
|
17
|
-
}
|
|
18
|
-
} catch { /* no .env */ }
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
function loadConfig(pluginConfig = {}, opts = {}) {
|
|
22
|
-
const home = opts.home || DEFAULT_HOME;
|
|
23
|
-
loadEnvFile(opts.envPath || path.join(home, '.env'));
|
|
24
|
-
|
|
25
|
-
let defaults = {};
|
|
26
|
-
const configPath = opts.configPath || path.join(home, 'extensions/afterburn/config.default.json');
|
|
27
|
-
try {
|
|
28
|
-
let raw = fs.readFileSync(configPath, 'utf8');
|
|
29
|
-
raw = raw.replace(/\$\{(\w+)\}/g, (_, key) => process.env[key] || '');
|
|
30
|
-
defaults = JSON.parse(raw);
|
|
31
|
-
} catch { /* use empty */ }
|
|
32
|
-
|
|
33
|
-
return { ...defaults['afterburn'], ...pluginConfig };
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
// Model defaults per runtime — Miranda's choices on MiniMax
|
|
37
|
-
const RUNTIME_DEFAULTS = {
|
|
38
|
-
gateway: 'MiniMax-M2.7',
|
|
39
|
-
cc: 'MiniMax-M2.5',
|
|
40
|
-
opencode: 'MiniMax-M2.5',
|
|
41
|
-
};
|
|
42
|
-
|
|
43
|
-
const RUNTIME_ENV_KEY = {
|
|
44
|
-
cc: 'CC_AFTERBURN_MODEL',
|
|
45
|
-
opencode: 'OPENCODE_AFTERBURN_MODEL',
|
|
46
|
-
};
|
|
47
|
-
|
|
48
|
-
function resolveModel({ runtime, explicitModel, configModel } = {}) {
|
|
49
|
-
if (explicitModel) return explicitModel;
|
|
50
|
-
const envKey = RUNTIME_ENV_KEY[runtime] || 'AFTERBURN_LLM_MODEL';
|
|
51
|
-
if (process.env[envKey]) return process.env[envKey];
|
|
52
|
-
if (configModel) return configModel;
|
|
53
|
-
return RUNTIME_DEFAULTS[runtime] || RUNTIME_DEFAULTS.gateway;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
async function callLlm(prompt, { runtime, model, timeoutMs } = {}) {
|
|
57
|
-
const resolvedModel = model || resolveModel({ runtime });
|
|
58
|
-
const timeout = timeoutMs || 120000;
|
|
59
|
-
const apiKey = process.env.MINIMAX_API_KEY || process.env.OPENCODE_API_KEY || '';
|
|
60
|
-
if (!apiKey) throw new Error('MINIMAX_API_KEY not set');
|
|
61
|
-
|
|
62
|
-
const controller = new AbortController();
|
|
63
|
-
const timer = setTimeout(() => controller.abort(), timeout);
|
|
64
|
-
try {
|
|
65
|
-
const res = await fetch('https://api.minimax.io/anthropic/v1/messages', {
|
|
66
|
-
method: 'POST',
|
|
67
|
-
headers: {
|
|
68
|
-
'Content-Type': 'application/json',
|
|
69
|
-
'Authorization': `Bearer ${apiKey}`,
|
|
70
|
-
'anthropic-version': '2023-06-01',
|
|
71
|
-
},
|
|
72
|
-
body: JSON.stringify({
|
|
73
|
-
model: resolvedModel,
|
|
74
|
-
messages: [{ role: 'user', content: prompt }],
|
|
75
|
-
max_tokens: 4096,
|
|
76
|
-
}),
|
|
77
|
-
signal: controller.signal,
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
if (!res.ok) {
|
|
81
|
-
const body = await res.text().catch(() => '');
|
|
82
|
-
throw new Error(`LLM ${res.status}: ${body.slice(0, 200)}`);
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
const data = await res.json();
|
|
86
|
-
let raw;
|
|
87
|
-
if (data.content && Array.isArray(data.content)) {
|
|
88
|
-
raw = data.content.map(c => c.text || '').join('');
|
|
89
|
-
} else {
|
|
90
|
-
raw = data.choices?.[0]?.message?.content || '';
|
|
91
|
-
}
|
|
92
|
-
// Strip <think>...</think> reasoning tags (MiniMax M2.5)
|
|
93
|
-
return raw.replace(/<think>[\s\S]*?<\/think>/g, '').trim();
|
|
94
|
-
} finally {
|
|
95
|
-
clearTimeout(timer);
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
module.exports = { loadEnvFile, loadConfig, resolveModel, callLlm };
|
|
@@ -1,145 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"$schema": "https://aquifer.dev/schema/consumer-profile.v1.json",
|
|
3
|
-
"consumer_profile_id": "miranda",
|
|
4
|
-
"version": 1,
|
|
5
|
-
"description": "Miranda persona consumer profile — canonical shape for session state, handoff, decision log, timeline categories, and default artifact producers. Reference implementation shipped inside Aquifer; production deployments may register additional versions with schema changes.",
|
|
6
|
-
"defaults": {
|
|
7
|
-
"tenant_id": "default",
|
|
8
|
-
"agent_id": "main",
|
|
9
|
-
"time_zone": "Asia/Taipei"
|
|
10
|
-
},
|
|
11
|
-
"schemas": {
|
|
12
|
-
"default.session_state.v1": {
|
|
13
|
-
"kind": "default",
|
|
14
|
-
"target": "sessionState",
|
|
15
|
-
"description": "What Miranda is currently focused on — goal + active threads + affect tag.",
|
|
16
|
-
"json_schema": {
|
|
17
|
-
"type": "object",
|
|
18
|
-
"additionalProperties": true,
|
|
19
|
-
"properties": {
|
|
20
|
-
"goal": { "type": ["string", "null"] },
|
|
21
|
-
"active_work": { "type": "array", "items": { "type": "string" } },
|
|
22
|
-
"blockers": { "type": "array", "items": { "type": "string" } },
|
|
23
|
-
"affect": {
|
|
24
|
-
"type": "object",
|
|
25
|
-
"additionalProperties": true,
|
|
26
|
-
"properties": {
|
|
27
|
-
"mood": { "type": ["string", "null"] },
|
|
28
|
-
"energy": { "enum": ["low", "medium", "high", null] },
|
|
29
|
-
"confidence": { "enum": ["low", "medium", "high", null] },
|
|
30
|
-
"notes": { "type": ["string", "null"] }
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
},
|
|
34
|
-
"required": ["goal", "active_work", "blockers", "affect"]
|
|
35
|
-
}
|
|
36
|
-
},
|
|
37
|
-
"default.session_handoff.v1": {
|
|
38
|
-
"kind": "default",
|
|
39
|
-
"target": "sessionHandoff",
|
|
40
|
-
"description": "Session-end baton: what was the last step, where to pick up, what still blocks.",
|
|
41
|
-
"json_schema": {
|
|
42
|
-
"type": "object",
|
|
43
|
-
"additionalProperties": true,
|
|
44
|
-
"properties": {
|
|
45
|
-
"last_step": { "type": "string" },
|
|
46
|
-
"status": { "enum": ["in_progress", "completed", "blocked"] },
|
|
47
|
-
"next": { "type": ["string", "null"] },
|
|
48
|
-
"blockers": { "type": "array", "items": { "type": "string" } },
|
|
49
|
-
"decided": { "type": "array", "items": { "type": "string" } },
|
|
50
|
-
"open_loops": { "type": "array", "items": { "type": "string" } }
|
|
51
|
-
},
|
|
52
|
-
"required": ["last_step", "status", "next", "blockers", "decided", "open_loops"]
|
|
53
|
-
}
|
|
54
|
-
},
|
|
55
|
-
"default.decision_log.v1": {
|
|
56
|
-
"kind": "default",
|
|
57
|
-
"target": "decisionLog",
|
|
58
|
-
"description": "Committed / proposed / reversed decisions with optional fact linkage.",
|
|
59
|
-
"json_schema": {
|
|
60
|
-
"type": "object",
|
|
61
|
-
"additionalProperties": true,
|
|
62
|
-
"properties": {
|
|
63
|
-
"decision": { "type": "string" },
|
|
64
|
-
"reason": { "type": ["string", "null"] },
|
|
65
|
-
"status": { "enum": ["proposed", "committed", "reversed"] },
|
|
66
|
-
"related_fact_ids": {
|
|
67
|
-
"type": "array",
|
|
68
|
-
"items": { "type": "integer" }
|
|
69
|
-
}
|
|
70
|
-
},
|
|
71
|
-
"required": ["decision", "reason", "status"]
|
|
72
|
-
}
|
|
73
|
-
},
|
|
74
|
-
"default.timeline.v1": {
|
|
75
|
-
"kind": "default",
|
|
76
|
-
"target": "timeline",
|
|
77
|
-
"description": "Miranda daily timeline category vocabulary. Categories map to existing daily-log sections (focus/todo/mood/handoff) plus organisational tags Miranda uses in weekly/monthly rollups.",
|
|
78
|
-
"category_vocabulary": [
|
|
79
|
-
"cli",
|
|
80
|
-
"focus",
|
|
81
|
-
"todo",
|
|
82
|
-
"handoff",
|
|
83
|
-
"narrative",
|
|
84
|
-
"organized",
|
|
85
|
-
"weekly",
|
|
86
|
-
"monthly",
|
|
87
|
-
"stats",
|
|
88
|
-
"health",
|
|
89
|
-
"garmin",
|
|
90
|
-
"note"
|
|
91
|
-
],
|
|
92
|
-
"json_schema": {
|
|
93
|
-
"type": "object",
|
|
94
|
-
"additionalProperties": false,
|
|
95
|
-
"properties": {
|
|
96
|
-
"occurred_at": { "type": "string", "format": "date-time" },
|
|
97
|
-
"source": { "type": "string" },
|
|
98
|
-
"session_ref": { "type": ["string", "null"] },
|
|
99
|
-
"category": { "type": "string" },
|
|
100
|
-
"text": { "type": "string" },
|
|
101
|
-
"metadata": { "type": "object" }
|
|
102
|
-
},
|
|
103
|
-
"required": ["occurred_at", "source", "session_ref", "category", "text", "metadata"]
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
},
|
|
107
|
-
"artifacts": {
|
|
108
|
-
"producers": [
|
|
109
|
-
{
|
|
110
|
-
"producer_id": "miranda.workspace.daily-log",
|
|
111
|
-
"type": "daily-log",
|
|
112
|
-
"trigger_phase": "timeline_write",
|
|
113
|
-
"format": "markdown",
|
|
114
|
-
"destination": "workspace://memory/{date}.md",
|
|
115
|
-
"description": "Renders the day's timeline events + state snapshot + handoff to a single markdown file under the Miranda workspace. Source of truth remains the DB; the .md is a rendered view."
|
|
116
|
-
},
|
|
117
|
-
{
|
|
118
|
-
"producer_id": "miranda.workspace.weekly-log",
|
|
119
|
-
"type": "weekly-log",
|
|
120
|
-
"trigger_phase": "artifact_dispatch",
|
|
121
|
-
"format": "markdown",
|
|
122
|
-
"destination": "workspace://memory/weekly/{week_start}.md",
|
|
123
|
-
"description": "Weekly rollup. Timeline events tagged [WEEKLY] plus cross-session narratives. Rendered from DB via rollupWeekly() helper."
|
|
124
|
-
},
|
|
125
|
-
{
|
|
126
|
-
"producer_id": "miranda.workspace.monthly-log",
|
|
127
|
-
"type": "monthly-log",
|
|
128
|
-
"trigger_phase": "artifact_dispatch",
|
|
129
|
-
"format": "markdown",
|
|
130
|
-
"destination": "workspace://memory/monthly/{month}.md",
|
|
131
|
-
"description": "Monthly rollup. Aggregates weekly entries + narrative supersede chain."
|
|
132
|
-
}
|
|
133
|
-
]
|
|
134
|
-
},
|
|
135
|
-
"extraction_hints": {
|
|
136
|
-
"entities": {
|
|
137
|
-
"include_types": ["person", "project", "tool", "topic"],
|
|
138
|
-
"exclude_patterns": ["^/tmp/", "^/home/mingko/\\.", "^node_modules/"]
|
|
139
|
-
},
|
|
140
|
-
"facts": {
|
|
141
|
-
"prefer_subjects": ["MK", "Miranda", "Aquifer", "OpenClaw", "Jenny", "Evan", "Ivan"],
|
|
142
|
-
"avoid_ephemeral": true
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
}
|