@remnic/plugin-openclaw 1.0.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/dist/calibration-KQXCC77L.js +235 -0
- package/dist/causal-chain-LA3IQNL6.js +22 -0
- package/dist/causal-consolidation-WINYJQJ4.js +205 -0
- package/dist/causal-retrieval-ITNQBUQM.js +182 -0
- package/dist/causal-trajectory-graph-7Z5DD66L.js +59 -0
- package/dist/chunk-5LE4HTVL.js +46 -0
- package/dist/chunk-BZ27H3BL.js +158 -0
- package/dist/chunk-DMGIUDBO.js +41 -0
- package/dist/chunk-GUSMRW4H.js +12 -0
- package/dist/chunk-H3SKMYPU.js +340 -0
- package/dist/chunk-HSBPDYF4.js +278 -0
- package/dist/chunk-JGEKL3WH.js +434 -0
- package/dist/chunk-MLKGABMK.js +9 -0
- package/dist/chunk-TJZ7KBCC.js +577 -0
- package/dist/chunk-WLR4WL6B.js +5893 -0
- package/dist/chunk-Y7JG2Q3V.js +4242 -0
- package/dist/engine-QHRKR53Q.js +11 -0
- package/dist/fallback-llm-2VMRPBHR.js +8 -0
- package/dist/index.js +61705 -0
- package/dist/legacy-hook-compat-XQ7FP6FV.js +35 -0
- package/dist/logger-NZE2OBVA.js +9 -0
- package/dist/storage-AGYBIME4.js +16 -0
- package/openclaw.plugin.json +3779 -0
- package/package.json +54 -0
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import {
|
|
2
|
+
FallbackLlmClient
|
|
3
|
+
} from "./chunk-TJZ7KBCC.js";
|
|
4
|
+
import {
|
|
5
|
+
listJsonFiles
|
|
6
|
+
} from "./chunk-5LE4HTVL.js";
|
|
7
|
+
import {
|
|
8
|
+
log
|
|
9
|
+
} from "./chunk-DMGIUDBO.js";
|
|
10
|
+
import "./chunk-MLKGABMK.js";
|
|
11
|
+
|
|
12
|
+
// ../remnic-core/src/calibration.ts
|
|
13
|
+
import { createHash } from "crypto";
|
|
14
|
+
import path from "path";
|
|
15
|
+
import { mkdir, readFile, writeFile } from "fs/promises";
|
|
16
|
+
function calibrationDir(memoryDir) {
|
|
17
|
+
return path.join(memoryDir, "state", "calibration");
|
|
18
|
+
}
|
|
19
|
+
function calibrationIndexPath(memoryDir) {
|
|
20
|
+
return path.join(calibrationDir(memoryDir), "calibration-index.json");
|
|
21
|
+
}
|
|
22
|
+
async function readCalibrationIndex(memoryDir) {
|
|
23
|
+
try {
|
|
24
|
+
const raw = JSON.parse(await readFile(calibrationIndexPath(memoryDir), "utf8"));
|
|
25
|
+
return {
|
|
26
|
+
rules: Array.isArray(raw.rules) ? raw.rules : [],
|
|
27
|
+
updatedAt: typeof raw.updatedAt === "string" ? raw.updatedAt : (/* @__PURE__ */ new Date()).toISOString(),
|
|
28
|
+
totalCorrectionsAnalyzed: typeof raw.totalCorrectionsAnalyzed === "number" ? raw.totalCorrectionsAnalyzed : 0
|
|
29
|
+
};
|
|
30
|
+
} catch {
|
|
31
|
+
return { rules: [], updatedAt: (/* @__PURE__ */ new Date()).toISOString(), totalCorrectionsAnalyzed: 0 };
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
async function writeCalibrationIndex(memoryDir, index) {
|
|
35
|
+
const dir = calibrationDir(memoryDir);
|
|
36
|
+
await mkdir(dir, { recursive: true });
|
|
37
|
+
index.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
38
|
+
await writeFile(calibrationIndexPath(memoryDir), JSON.stringify(index, null, 2), "utf8");
|
|
39
|
+
}
|
|
40
|
+
async function readCorrections(memoryDir) {
|
|
41
|
+
const correctionsDir = path.join(memoryDir, "corrections");
|
|
42
|
+
const files = await listJsonFiles(correctionsDir).catch(() => {
|
|
43
|
+
return [];
|
|
44
|
+
});
|
|
45
|
+
const factsDir = path.join(memoryDir, "facts");
|
|
46
|
+
try {
|
|
47
|
+
const { readdir } = await import("fs/promises");
|
|
48
|
+
const dayDirs = (await readdir(factsDir)).filter((d) => /^\d{4}-\d{2}-\d{2}$/.test(d));
|
|
49
|
+
for (const day of dayDirs) {
|
|
50
|
+
const dayPath = path.join(factsDir, day);
|
|
51
|
+
const dayFiles = (await readdir(dayPath)).filter((f) => f.startsWith("correction-") && f.endsWith(".md")).map((f) => path.join(dayPath, f));
|
|
52
|
+
files.push(...dayFiles);
|
|
53
|
+
}
|
|
54
|
+
} catch {
|
|
55
|
+
}
|
|
56
|
+
try {
|
|
57
|
+
const { readdir } = await import("fs/promises");
|
|
58
|
+
const corrFiles = (await readdir(correctionsDir)).filter((f) => f.endsWith(".md")).map((f) => path.join(correctionsDir, f));
|
|
59
|
+
files.push(...corrFiles);
|
|
60
|
+
} catch {
|
|
61
|
+
}
|
|
62
|
+
const corrections = [];
|
|
63
|
+
const seen = /* @__PURE__ */ new Set();
|
|
64
|
+
for (const filePath of files) {
|
|
65
|
+
try {
|
|
66
|
+
const raw = await readFile(filePath, "utf8");
|
|
67
|
+
const fmMatch = raw.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
|
|
68
|
+
if (!fmMatch) continue;
|
|
69
|
+
const content = fmMatch[2].trim();
|
|
70
|
+
if (!content || content.length < 10) continue;
|
|
71
|
+
const idMatch = fmMatch[1].match(/^id:\s*(.+)$/m);
|
|
72
|
+
const id = idMatch?.[1]?.trim() ?? path.basename(filePath, ".md");
|
|
73
|
+
if (seen.has(id)) continue;
|
|
74
|
+
seen.add(id);
|
|
75
|
+
const confMatch = fmMatch[1].match(/^confidence:\s*(.+)$/m);
|
|
76
|
+
const confidence = confMatch ? parseFloat(confMatch[1]) : 0.9;
|
|
77
|
+
const entityMatch = fmMatch[1].match(/^entityRef:\s*(.+)$/m);
|
|
78
|
+
const entityRefs = entityMatch ? [entityMatch[1].trim()] : [];
|
|
79
|
+
corrections.push({ id, content, created: "", confidence, entityRefs, tags: [] });
|
|
80
|
+
} catch {
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return corrections;
|
|
84
|
+
}
|
|
85
|
+
var CLUSTER_PROMPT = `You are analyzing user corrections to an AI assistant. Each correction represents a moment where the assistant's prediction of what the user wanted was WRONG.
|
|
86
|
+
|
|
87
|
+
Your job: Group these corrections into clusters where the SAME TYPE of misunderstanding is happening. Then for each cluster, synthesize a CalibrationRule.
|
|
88
|
+
|
|
89
|
+
A CalibrationRule describes:
|
|
90
|
+
- condition: When does this type of mistake happen?
|
|
91
|
+
- modelTendency: What does the model tend to assume or do wrong?
|
|
92
|
+
- userExpectation: What does the user actually want instead?
|
|
93
|
+
- calibration: How should the model adjust its behavior?
|
|
94
|
+
- ruleType: One of "model_tendency", "user_expectation", "scope_boundary", "verification_required"
|
|
95
|
+
|
|
96
|
+
Focus on PATTERNS, not individual corrections. A cluster needs at least 2 corrections to be worth a rule.
|
|
97
|
+
|
|
98
|
+
Output valid JSON only:
|
|
99
|
+
{
|
|
100
|
+
"rules": [
|
|
101
|
+
{
|
|
102
|
+
"ruleType": "model_tendency",
|
|
103
|
+
"condition": "When discussing project scope or task boundaries",
|
|
104
|
+
"modelTendency": "The model tends to assume broader scope than the user intends",
|
|
105
|
+
"userExpectation": "The user prefers narrow, specific task definitions and wants to be asked before scope expansion",
|
|
106
|
+
"calibration": "When uncertain about scope, ask for clarification rather than assuming. Default to the narrower interpretation.",
|
|
107
|
+
"confidence": 0.85,
|
|
108
|
+
"evidenceIds": ["correction-id-1", "correction-id-2"]
|
|
109
|
+
}
|
|
110
|
+
]
|
|
111
|
+
}`;
|
|
112
|
+
async function synthesizeCalibrationRules(corrections, llm, existingRules, agentId) {
|
|
113
|
+
if (corrections.length < 2) return [];
|
|
114
|
+
const correctionText = corrections.slice(0, 50).map((c, i) => `[${c.id}] ${c.content}`).join("\n\n");
|
|
115
|
+
const existingRulesText = existingRules.length > 0 ? `
|
|
116
|
+
|
|
117
|
+
Existing calibration rules (avoid duplicating these):
|
|
118
|
+
${existingRules.map((r) => `- ${r.condition}: ${r.calibration}`).join("\n")}` : "";
|
|
119
|
+
const response = await llm.chatCompletion(
|
|
120
|
+
[
|
|
121
|
+
{ role: "system", content: CLUSTER_PROMPT },
|
|
122
|
+
{ role: "user", content: `Here are ${corrections.length} corrections from this user:
|
|
123
|
+
|
|
124
|
+
${correctionText}${existingRulesText}` }
|
|
125
|
+
],
|
|
126
|
+
{ temperature: 0.3, maxTokens: 3e3, agentId }
|
|
127
|
+
);
|
|
128
|
+
if (!response?.content) return [];
|
|
129
|
+
try {
|
|
130
|
+
let jsonStr = response.content.trim();
|
|
131
|
+
const fenceMatch = jsonStr.match(/```(?:json)?\s*\n?([\s\S]*?)\n?\s*```/);
|
|
132
|
+
if (fenceMatch) jsonStr = fenceMatch[1];
|
|
133
|
+
const parsed = JSON.parse(jsonStr);
|
|
134
|
+
if (!Array.isArray(parsed.rules)) return [];
|
|
135
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
136
|
+
return parsed.rules.filter((r) => r.condition && r.calibration && r.modelTendency).map((r) => ({
|
|
137
|
+
id: `cal-${createHash("sha256").update(r.condition + r.calibration).digest("hex").slice(0, 12)}`,
|
|
138
|
+
ruleType: r.ruleType ?? "model_tendency",
|
|
139
|
+
condition: String(r.condition),
|
|
140
|
+
modelTendency: String(r.modelTendency),
|
|
141
|
+
userExpectation: String(r.userExpectation ?? ""),
|
|
142
|
+
calibration: String(r.calibration),
|
|
143
|
+
confidence: typeof r.confidence === "number" ? r.confidence : 0.7,
|
|
144
|
+
evidenceCount: Array.isArray(r.evidenceIds) ? r.evidenceIds.length : 1,
|
|
145
|
+
evidenceCorrectionIds: Array.isArray(r.evidenceIds) ? r.evidenceIds : [],
|
|
146
|
+
createdAt: now,
|
|
147
|
+
lastReinforcedAt: now
|
|
148
|
+
}));
|
|
149
|
+
} catch {
|
|
150
|
+
log.warn("[calibration] failed to parse LLM response");
|
|
151
|
+
return [];
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
function buildCalibrationRecallSection(rules, query, maxChars = 1200) {
|
|
155
|
+
if (rules.length === 0) return null;
|
|
156
|
+
const lines = [
|
|
157
|
+
"## Model Calibration (learned from past corrections)",
|
|
158
|
+
"",
|
|
159
|
+
"Adjustments for this specific user, learned from patterns in their corrections:",
|
|
160
|
+
""
|
|
161
|
+
];
|
|
162
|
+
let totalChars = lines.join("\n").length;
|
|
163
|
+
for (const rule of rules) {
|
|
164
|
+
const line = `- **${rule.condition}**: ${rule.modelTendency} \u2192 Instead: ${rule.calibration}`;
|
|
165
|
+
if (totalChars + line.length + 1 > maxChars) break;
|
|
166
|
+
lines.push(line);
|
|
167
|
+
totalChars += line.length + 1;
|
|
168
|
+
}
|
|
169
|
+
if (lines.length <= 4) return null;
|
|
170
|
+
lines.push("");
|
|
171
|
+
return lines.join("\n");
|
|
172
|
+
}
|
|
173
|
+
async function runCalibrationConsolidation(options) {
|
|
174
|
+
try {
|
|
175
|
+
const llm = new FallbackLlmClient(options.gatewayConfig);
|
|
176
|
+
if (!llm.isAvailable(options.gatewayAgentId)) {
|
|
177
|
+
log.debug("[calibration] no LLM available \u2014 skipping consolidation");
|
|
178
|
+
return [];
|
|
179
|
+
}
|
|
180
|
+
const corrections = await readCorrections(options.memoryDir);
|
|
181
|
+
if (corrections.length < 3) {
|
|
182
|
+
log.debug(`[calibration] only ${corrections.length} corrections \u2014 need at least 3`);
|
|
183
|
+
return [];
|
|
184
|
+
}
|
|
185
|
+
const existingIndex = await readCalibrationIndex(options.memoryDir);
|
|
186
|
+
const newRules = await synthesizeCalibrationRules(corrections, llm, existingIndex.rules, options.gatewayAgentId);
|
|
187
|
+
if (newRules.length === 0) {
|
|
188
|
+
log.debug("[calibration] no new calibration rules synthesized");
|
|
189
|
+
return existingIndex.rules;
|
|
190
|
+
}
|
|
191
|
+
const ruleMap = new Map(existingIndex.rules.map((r) => [r.id, r]));
|
|
192
|
+
for (const rule of newRules) {
|
|
193
|
+
if (ruleMap.has(rule.id)) {
|
|
194
|
+
const existing = ruleMap.get(rule.id);
|
|
195
|
+
existing.lastReinforcedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
196
|
+
existing.evidenceCount += rule.evidenceCount;
|
|
197
|
+
existing.confidence = Math.min(1, existing.confidence + 0.05);
|
|
198
|
+
} else {
|
|
199
|
+
ruleMap.set(rule.id, rule);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
const allRules = [...ruleMap.values()];
|
|
203
|
+
await writeCalibrationIndex(options.memoryDir, {
|
|
204
|
+
rules: allRules,
|
|
205
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
206
|
+
totalCorrectionsAnalyzed: corrections.length
|
|
207
|
+
});
|
|
208
|
+
log.debug(`[calibration] synthesized ${newRules.length} new rule(s), ${allRules.length} total`);
|
|
209
|
+
return allRules;
|
|
210
|
+
} catch (error) {
|
|
211
|
+
log.warn(`[calibration] consolidation failed (non-fatal): ${error instanceof Error ? error.message : String(error)}`);
|
|
212
|
+
return [];
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
async function runCalibrationIfEnabled(options) {
|
|
216
|
+
if (!options.calibrationEnabled) {
|
|
217
|
+
return [];
|
|
218
|
+
}
|
|
219
|
+
return runCalibrationConsolidation({
|
|
220
|
+
memoryDir: options.memoryDir,
|
|
221
|
+
gatewayConfig: options.gatewayConfig
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
async function getCalibrationRulesForRecall(memoryDir) {
|
|
225
|
+
const index = await readCalibrationIndex(memoryDir);
|
|
226
|
+
return index.rules;
|
|
227
|
+
}
|
|
228
|
+
export {
|
|
229
|
+
buildCalibrationRecallSection,
|
|
230
|
+
getCalibrationRulesForRecall,
|
|
231
|
+
readCalibrationIndex,
|
|
232
|
+
runCalibrationConsolidation,
|
|
233
|
+
runCalibrationIfEnabled,
|
|
234
|
+
synthesizeCalibrationRules
|
|
235
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import {
|
|
2
|
+
makeEdgeId,
|
|
3
|
+
readChainIndex,
|
|
4
|
+
resolveChainsDir,
|
|
5
|
+
scoreStitchCandidate,
|
|
6
|
+
stitchCausalChain,
|
|
7
|
+
validateCausalEdge,
|
|
8
|
+
writeChainIndex
|
|
9
|
+
} from "./chunk-HSBPDYF4.js";
|
|
10
|
+
import "./chunk-JGEKL3WH.js";
|
|
11
|
+
import "./chunk-5LE4HTVL.js";
|
|
12
|
+
import "./chunk-DMGIUDBO.js";
|
|
13
|
+
import "./chunk-MLKGABMK.js";
|
|
14
|
+
export {
|
|
15
|
+
makeEdgeId,
|
|
16
|
+
readChainIndex,
|
|
17
|
+
resolveChainsDir,
|
|
18
|
+
scoreStitchCandidate,
|
|
19
|
+
stitchCausalChain,
|
|
20
|
+
validateCausalEdge,
|
|
21
|
+
writeChainIndex
|
|
22
|
+
};
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import {
|
|
2
|
+
readChainIndex,
|
|
3
|
+
resolveChainsDir
|
|
4
|
+
} from "./chunk-HSBPDYF4.js";
|
|
5
|
+
import {
|
|
6
|
+
isRecord
|
|
7
|
+
} from "./chunk-JGEKL3WH.js";
|
|
8
|
+
import {
|
|
9
|
+
FallbackLlmClient
|
|
10
|
+
} from "./chunk-TJZ7KBCC.js";
|
|
11
|
+
import {
|
|
12
|
+
listJsonFiles,
|
|
13
|
+
readJsonFile
|
|
14
|
+
} from "./chunk-5LE4HTVL.js";
|
|
15
|
+
import {
|
|
16
|
+
log
|
|
17
|
+
} from "./chunk-DMGIUDBO.js";
|
|
18
|
+
import "./chunk-MLKGABMK.js";
|
|
19
|
+
|
|
20
|
+
// ../remnic-core/src/causal-consolidation.ts
|
|
21
|
+
import { createHash } from "crypto";
|
|
22
|
+
import path from "path";
|
|
23
|
+
async function readAllTrajectories(memoryDir, causalTrajectoryStoreDir) {
|
|
24
|
+
const root = causalTrajectoryStoreDir ? path.join(memoryDir, causalTrajectoryStoreDir) : path.join(memoryDir, "state", "causal-trajectories");
|
|
25
|
+
const trajectoriesDir = path.join(root, "trajectories");
|
|
26
|
+
const files = await listJsonFiles(trajectoriesDir).catch(() => []);
|
|
27
|
+
const results = [];
|
|
28
|
+
for (const filePath of files) {
|
|
29
|
+
try {
|
|
30
|
+
const raw = await readJsonFile(filePath);
|
|
31
|
+
if (isRecord(raw) && typeof raw.trajectoryId === "string") {
|
|
32
|
+
results.push(raw);
|
|
33
|
+
}
|
|
34
|
+
} catch {
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return results;
|
|
38
|
+
}
|
|
39
|
+
function formatCausalContext(trajectories, chainIndex, maxChars = 8e3) {
|
|
40
|
+
const bySession = /* @__PURE__ */ new Map();
|
|
41
|
+
for (const t of trajectories) {
|
|
42
|
+
const list = bySession.get(t.sessionKey) ?? [];
|
|
43
|
+
list.push(t);
|
|
44
|
+
bySession.set(t.sessionKey, list);
|
|
45
|
+
}
|
|
46
|
+
const lines = [];
|
|
47
|
+
lines.push(`## Causal Trajectories (${trajectories.length} across ${bySession.size} sessions)`);
|
|
48
|
+
lines.push("");
|
|
49
|
+
for (const [sessionKey, sessionTrajs] of bySession) {
|
|
50
|
+
lines.push(`### Session: ${sessionKey}`);
|
|
51
|
+
for (const t of sessionTrajs.slice(0, 5)) {
|
|
52
|
+
const outcome = t.outcomeKind === "success" ? "+" : t.outcomeKind === "failure" ? "-" : "~";
|
|
53
|
+
lines.push(`[${outcome}] Goal: ${t.goal}`);
|
|
54
|
+
lines.push(` Action: ${t.actionSummary}`);
|
|
55
|
+
lines.push(` Outcome: ${t.outcomeSummary}`);
|
|
56
|
+
if (t.followUpSummary) lines.push(` Follow-up: ${t.followUpSummary}`);
|
|
57
|
+
if (t.entityRefs?.length) lines.push(` Entities: ${t.entityRefs.join(", ")}`);
|
|
58
|
+
}
|
|
59
|
+
lines.push("");
|
|
60
|
+
}
|
|
61
|
+
const edgeCount = Object.keys(chainIndex.edges).length;
|
|
62
|
+
if (edgeCount > 0) {
|
|
63
|
+
lines.push(`## Cross-Session Causal Chains (${edgeCount} connections)`);
|
|
64
|
+
lines.push("");
|
|
65
|
+
const trajectoryMap = new Map(trajectories.map((t) => [t.trajectoryId, t]));
|
|
66
|
+
const shown = /* @__PURE__ */ new Set();
|
|
67
|
+
for (const [edgeId, edge] of Object.entries(chainIndex.edges)) {
|
|
68
|
+
if (shown.size >= 10) break;
|
|
69
|
+
const from = trajectoryMap.get(edge.fromTrajectoryId);
|
|
70
|
+
const to = trajectoryMap.get(edge.toTrajectoryId);
|
|
71
|
+
if (!from || !to) continue;
|
|
72
|
+
lines.push(`${edge.edgeType}: "${from.goal}" (${from.sessionKey}) \u2192 "${to.goal}" (${to.sessionKey})`);
|
|
73
|
+
shown.add(edgeId);
|
|
74
|
+
}
|
|
75
|
+
lines.push("");
|
|
76
|
+
}
|
|
77
|
+
const result = lines.join("\n");
|
|
78
|
+
return result.length > maxChars ? result.slice(0, maxChars) + "\n[truncated]" : result;
|
|
79
|
+
}
|
|
80
|
+
var CONSOLIDATION_PROMPT = `You are analyzing a user's causal trajectory history across multiple sessions. Trajectories record what the user tried to do (goal), what they did (action), and what happened (outcome).
|
|
81
|
+
|
|
82
|
+
Your job is to identify:
|
|
83
|
+
1. BEHAVIORAL RULES: Recurring patterns where the same approach consistently succeeds or fails. These should be actionable guidance for future sessions.
|
|
84
|
+
2. PREFERENCES: What the user cares about, prefers, or consistently chooses \u2014 even if never explicitly stated. Infer preferences from what they repeatedly do, retry until successful, or always include in their workflow.
|
|
85
|
+
|
|
86
|
+
IMPORTANT:
|
|
87
|
+
- Look for CROSS-SESSION patterns \u2014 things that repeat across different sessions are more significant than within-session patterns.
|
|
88
|
+
- A user who retries the same goal across sessions has a strong implicit preference for that outcome.
|
|
89
|
+
- Consistent action choices reveal preferences even when the user never says "I prefer X."
|
|
90
|
+
- Frame preferences as "The user would prefer responses that..." when applicable.
|
|
91
|
+
|
|
92
|
+
Output valid JSON only:
|
|
93
|
+
{
|
|
94
|
+
"rules": [
|
|
95
|
+
{"content": "actionable rule text", "category": "rule|principle", "confidence": 0.0-1.0, "evidence": ["trajectory IDs"]}
|
|
96
|
+
],
|
|
97
|
+
"preferences": [
|
|
98
|
+
{"statement": "The user would prefer...", "confidence": 0.0-1.0, "evidence": ["trajectory IDs"]}
|
|
99
|
+
]
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
If no clear patterns exist, return {"rules": [], "preferences": []}.`;
|
|
103
|
+
async function consolidateWithLlm(context, llm, agentId) {
|
|
104
|
+
const response = await llm.chatCompletion(
|
|
105
|
+
[
|
|
106
|
+
{ role: "system", content: CONSOLIDATION_PROMPT },
|
|
107
|
+
{ role: "user", content: context }
|
|
108
|
+
],
|
|
109
|
+
{ temperature: 0.2, maxTokens: 2e3, agentId }
|
|
110
|
+
);
|
|
111
|
+
if (!response?.content) {
|
|
112
|
+
return { rules: [], preferences: [] };
|
|
113
|
+
}
|
|
114
|
+
try {
|
|
115
|
+
let jsonStr = response.content.trim();
|
|
116
|
+
const fenceMatch = jsonStr.match(/```(?:json)?\s*\n?([\s\S]*?)\n?\s*```/);
|
|
117
|
+
if (fenceMatch) jsonStr = fenceMatch[1];
|
|
118
|
+
const parsed = JSON.parse(jsonStr);
|
|
119
|
+
return {
|
|
120
|
+
rules: Array.isArray(parsed.rules) ? parsed.rules.filter(
|
|
121
|
+
(r) => typeof r.content === "string" && r.content.length > 5
|
|
122
|
+
) : [],
|
|
123
|
+
preferences: Array.isArray(parsed.preferences) ? parsed.preferences.filter(
|
|
124
|
+
(p) => typeof p.statement === "string" && p.statement.length > 5
|
|
125
|
+
) : []
|
|
126
|
+
};
|
|
127
|
+
} catch {
|
|
128
|
+
log.warn("[cmc] failed to parse LLM consolidation response");
|
|
129
|
+
return { rules: [], preferences: [] };
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
function stablePatternId(content) {
|
|
133
|
+
const digest = createHash("sha256").update(`causal-pattern\0${content}`).digest("hex").slice(0, 16);
|
|
134
|
+
return `causal-pattern:${digest}`;
|
|
135
|
+
}
|
|
136
|
+
function llmResultToCandidates(result) {
|
|
137
|
+
const candidates = [];
|
|
138
|
+
for (const rule of result.rules) {
|
|
139
|
+
const category = rule.category === "principle" ? "principle" : "rule";
|
|
140
|
+
candidates.push({
|
|
141
|
+
id: stablePatternId(rule.content),
|
|
142
|
+
sourceType: "causal-pattern",
|
|
143
|
+
subject: rule.content.slice(0, 80),
|
|
144
|
+
category,
|
|
145
|
+
content: rule.content,
|
|
146
|
+
score: Math.min(1, rule.confidence ?? 0.7),
|
|
147
|
+
rationale: "LLM-identified causal pattern from cross-session trajectory analysis",
|
|
148
|
+
outcome: null,
|
|
149
|
+
provenance: (rule.evidence ?? []).slice(0, 5),
|
|
150
|
+
agent: null,
|
|
151
|
+
workflow: null
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
return candidates;
|
|
155
|
+
}
|
|
156
|
+
async function deriveCausalPromotionCandidates(options) {
|
|
157
|
+
try {
|
|
158
|
+
const trajectories = await readAllTrajectories(options.memoryDir, options.causalTrajectoryStoreDir);
|
|
159
|
+
if (trajectories.length < options.config.minRecurrence) return [];
|
|
160
|
+
const chainsDir = resolveChainsDir(options.memoryDir, options.causalTrajectoryStoreDir);
|
|
161
|
+
const chainIndex = await readChainIndex(chainsDir);
|
|
162
|
+
const context = formatCausalContext(trajectories, chainIndex);
|
|
163
|
+
const llm = new FallbackLlmClient(options.gatewayConfig);
|
|
164
|
+
if (!llm.isAvailable(options.gatewayAgentId)) {
|
|
165
|
+
log.debug("[cmc] no LLM available for consolidation \u2014 skipping");
|
|
166
|
+
return [];
|
|
167
|
+
}
|
|
168
|
+
const result = await consolidateWithLlm(context, llm, options.gatewayAgentId);
|
|
169
|
+
const candidates = llmResultToCandidates(result);
|
|
170
|
+
log.debug(`[cmc] LLM consolidation produced ${candidates.length} rule(s) and ${result.preferences.length} preference(s)`);
|
|
171
|
+
return candidates;
|
|
172
|
+
} catch (error) {
|
|
173
|
+
log.warn(`[cmc] consolidation failed (non-fatal): ${error instanceof Error ? error.message : String(error)}`);
|
|
174
|
+
return [];
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
async function synthesizeCausalPreferencesViaLlm(options) {
|
|
178
|
+
try {
|
|
179
|
+
const trajectories = await readAllTrajectories(options.memoryDir, options.causalTrajectoryStoreDir);
|
|
180
|
+
if (trajectories.length < (options.minTrajectories ?? 2)) return null;
|
|
181
|
+
const chainsDir = resolveChainsDir(options.memoryDir, options.causalTrajectoryStoreDir);
|
|
182
|
+
const chainIndex = await readChainIndex(chainsDir);
|
|
183
|
+
const context = formatCausalContext(trajectories, chainIndex);
|
|
184
|
+
const llm = new FallbackLlmClient(options.gatewayConfig);
|
|
185
|
+
if (!llm.isAvailable(options.gatewayAgentId)) return null;
|
|
186
|
+
const result = await consolidateWithLlm(context, llm, options.gatewayAgentId);
|
|
187
|
+
if (result.preferences.length === 0 && result.rules.length === 0) return null;
|
|
188
|
+
const lines = ["## Behavioral Insights (from Causal Chain Analysis)", ""];
|
|
189
|
+
for (const pref of result.preferences) {
|
|
190
|
+
lines.push(`- ${pref.statement}`);
|
|
191
|
+
}
|
|
192
|
+
for (const rule of result.rules) {
|
|
193
|
+
lines.push(`- ${rule.content}`);
|
|
194
|
+
}
|
|
195
|
+
lines.push("");
|
|
196
|
+
return lines.join("\n");
|
|
197
|
+
} catch (error) {
|
|
198
|
+
log.warn(`[cmc] preference synthesis failed (non-fatal): ${error instanceof Error ? error.message : String(error)}`);
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
export {
|
|
203
|
+
deriveCausalPromotionCandidates,
|
|
204
|
+
synthesizeCausalPreferencesViaLlm
|
|
205
|
+
};
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import {
|
|
2
|
+
searchCausalTrajectories
|
|
3
|
+
} from "./chunk-BZ27H3BL.js";
|
|
4
|
+
import {
|
|
5
|
+
readChainIndex,
|
|
6
|
+
resolveChainsDir
|
|
7
|
+
} from "./chunk-HSBPDYF4.js";
|
|
8
|
+
import "./chunk-JGEKL3WH.js";
|
|
9
|
+
import "./chunk-5LE4HTVL.js";
|
|
10
|
+
import {
|
|
11
|
+
log
|
|
12
|
+
} from "./chunk-DMGIUDBO.js";
|
|
13
|
+
import "./chunk-MLKGABMK.js";
|
|
14
|
+
|
|
15
|
+
// ../remnic-core/src/causal-retrieval.ts
|
|
16
|
+
function walkUpstream(seedId, index, maxDepth, counterfactualBoost) {
|
|
17
|
+
const visited = /* @__PURE__ */ new Set([seedId]);
|
|
18
|
+
const results = [];
|
|
19
|
+
let frontier = [seedId];
|
|
20
|
+
for (let depth = 1; depth <= maxDepth; depth++) {
|
|
21
|
+
const nextFrontier = [];
|
|
22
|
+
for (const currentId of frontier) {
|
|
23
|
+
const incomingEdgeIds = index.incoming[currentId] ?? [];
|
|
24
|
+
for (const edgeId of incomingEdgeIds) {
|
|
25
|
+
const edge = index.edges[edgeId];
|
|
26
|
+
if (!edge) continue;
|
|
27
|
+
const fromId = edge.fromTrajectoryId;
|
|
28
|
+
if (visited.has(fromId)) continue;
|
|
29
|
+
visited.add(fromId);
|
|
30
|
+
const outgoingFromSource = index.outgoing[fromId] ?? [];
|
|
31
|
+
const isCounterfactual = outgoingFromSource.length > 1;
|
|
32
|
+
const depthDecay = 1 / depth;
|
|
33
|
+
let score = depthDecay * edge.confidence;
|
|
34
|
+
if (isCounterfactual) score += counterfactualBoost;
|
|
35
|
+
results.push({
|
|
36
|
+
trajectoryId: fromId,
|
|
37
|
+
depth,
|
|
38
|
+
score,
|
|
39
|
+
edgeType: edge.edgeType,
|
|
40
|
+
edgeConfidence: edge.confidence,
|
|
41
|
+
isCounterfactual
|
|
42
|
+
});
|
|
43
|
+
nextFrontier.push(fromId);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
frontier = nextFrontier;
|
|
47
|
+
if (frontier.length === 0) break;
|
|
48
|
+
}
|
|
49
|
+
return results;
|
|
50
|
+
}
|
|
51
|
+
function walkDownstream(seedId, index, maxDepth, counterfactualBoost) {
|
|
52
|
+
const downstreamMaxDepth = Math.min(maxDepth, 2);
|
|
53
|
+
const visited = /* @__PURE__ */ new Set([seedId]);
|
|
54
|
+
const results = [];
|
|
55
|
+
let frontier = [seedId];
|
|
56
|
+
for (let depth = 1; depth <= downstreamMaxDepth; depth++) {
|
|
57
|
+
const nextFrontier = [];
|
|
58
|
+
for (const currentId of frontier) {
|
|
59
|
+
const outgoingEdgeIds = index.outgoing[currentId] ?? [];
|
|
60
|
+
const isCounterfactual = outgoingEdgeIds.length > 1;
|
|
61
|
+
for (const edgeId of outgoingEdgeIds) {
|
|
62
|
+
const edge = index.edges[edgeId];
|
|
63
|
+
if (!edge) continue;
|
|
64
|
+
const toId = edge.toTrajectoryId;
|
|
65
|
+
if (visited.has(toId)) continue;
|
|
66
|
+
visited.add(toId);
|
|
67
|
+
const depthDecay = 1 / (depth + 1);
|
|
68
|
+
let score = depthDecay * edge.confidence;
|
|
69
|
+
if (isCounterfactual) score += counterfactualBoost;
|
|
70
|
+
results.push({
|
|
71
|
+
trajectoryId: toId,
|
|
72
|
+
depth,
|
|
73
|
+
score,
|
|
74
|
+
edgeType: edge.edgeType,
|
|
75
|
+
edgeConfidence: edge.confidence,
|
|
76
|
+
isCounterfactual
|
|
77
|
+
});
|
|
78
|
+
nextFrontier.push(toId);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
frontier = nextFrontier;
|
|
82
|
+
if (frontier.length === 0) break;
|
|
83
|
+
}
|
|
84
|
+
return results;
|
|
85
|
+
}
|
|
86
|
+
function formatRetrievalResult(result) {
|
|
87
|
+
const direction = result.direction === "upstream" ? "\u2191" : result.direction === "downstream" ? "\u2193" : "\u2022";
|
|
88
|
+
const counterfactual = result.isCounterfactual ? " [branching point]" : "";
|
|
89
|
+
const edgeInfo = result.edgeType ? ` (${result.edgeType})` : "";
|
|
90
|
+
return `- ${direction} ${result.summary}${edgeInfo}${counterfactual}`;
|
|
91
|
+
}
|
|
92
|
+
function formatCausalRetrievalSection(results, maxChars) {
|
|
93
|
+
if (results.length === 0) return null;
|
|
94
|
+
const lines = [
|
|
95
|
+
"## Causal Chain Context",
|
|
96
|
+
"",
|
|
97
|
+
"Related causal trajectories from connected sessions:",
|
|
98
|
+
""
|
|
99
|
+
];
|
|
100
|
+
let totalChars = lines.join("\n").length;
|
|
101
|
+
for (const result of results) {
|
|
102
|
+
const line = formatRetrievalResult(result);
|
|
103
|
+
if (totalChars + line.length + 1 > maxChars) break;
|
|
104
|
+
lines.push(line);
|
|
105
|
+
totalChars += line.length + 1;
|
|
106
|
+
}
|
|
107
|
+
if (lines.length <= 4) return null;
|
|
108
|
+
lines.push("");
|
|
109
|
+
return lines.join("\n");
|
|
110
|
+
}
|
|
111
|
+
async function retrieveCausalChains(options) {
|
|
112
|
+
try {
|
|
113
|
+
const { memoryDir, causalTrajectoryStoreDir, query, sessionKey, config: retrievalConfig } = options;
|
|
114
|
+
const seeds = await searchCausalTrajectories({
|
|
115
|
+
memoryDir,
|
|
116
|
+
causalTrajectoryStoreDir,
|
|
117
|
+
query,
|
|
118
|
+
maxResults: 3,
|
|
119
|
+
sessionKey
|
|
120
|
+
});
|
|
121
|
+
if (seeds.length === 0) return null;
|
|
122
|
+
const chainsDir = resolveChainsDir(memoryDir, causalTrajectoryStoreDir);
|
|
123
|
+
const chainIndex = await readChainIndex(chainsDir);
|
|
124
|
+
if (Object.keys(chainIndex.edges).length === 0) return null;
|
|
125
|
+
const allResults = [];
|
|
126
|
+
for (const seed of seeds) {
|
|
127
|
+
allResults.push({
|
|
128
|
+
trajectoryId: seed.record.trajectoryId,
|
|
129
|
+
direction: "seed",
|
|
130
|
+
depth: 0,
|
|
131
|
+
score: seed.score,
|
|
132
|
+
isCounterfactual: false,
|
|
133
|
+
summary: `[${seed.record.outcomeKind}] ${seed.record.goal} \u2192 ${seed.record.outcomeSummary}`
|
|
134
|
+
});
|
|
135
|
+
const upstream = walkUpstream(
|
|
136
|
+
seed.record.trajectoryId,
|
|
137
|
+
chainIndex,
|
|
138
|
+
retrievalConfig.maxDepth,
|
|
139
|
+
retrievalConfig.counterfactualBoost
|
|
140
|
+
);
|
|
141
|
+
for (const u of upstream) {
|
|
142
|
+
allResults.push({
|
|
143
|
+
trajectoryId: u.trajectoryId,
|
|
144
|
+
direction: "upstream",
|
|
145
|
+
depth: u.depth,
|
|
146
|
+
score: u.score,
|
|
147
|
+
edgeType: u.edgeType,
|
|
148
|
+
edgeConfidence: u.edgeConfidence,
|
|
149
|
+
isCounterfactual: u.isCounterfactual,
|
|
150
|
+
summary: `Depth ${u.depth}: trajectory ${u.trajectoryId.slice(0, 12)}`
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
const downstream = walkDownstream(
|
|
154
|
+
seed.record.trajectoryId,
|
|
155
|
+
chainIndex,
|
|
156
|
+
retrievalConfig.maxDepth,
|
|
157
|
+
retrievalConfig.counterfactualBoost
|
|
158
|
+
);
|
|
159
|
+
for (const d of downstream) {
|
|
160
|
+
allResults.push({
|
|
161
|
+
trajectoryId: d.trajectoryId,
|
|
162
|
+
direction: "downstream",
|
|
163
|
+
depth: d.depth,
|
|
164
|
+
score: d.score,
|
|
165
|
+
edgeType: d.edgeType,
|
|
166
|
+
edgeConfidence: d.edgeConfidence,
|
|
167
|
+
isCounterfactual: d.isCounterfactual,
|
|
168
|
+
summary: `Depth ${d.depth}: trajectory ${d.trajectoryId.slice(0, 12)}`
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
allResults.sort((a, b) => b.score - a.score);
|
|
173
|
+
return formatCausalRetrievalSection(allResults, retrievalConfig.maxChars);
|
|
174
|
+
} catch (error) {
|
|
175
|
+
log.warn(`[cmc] causal retrieval failed (non-fatal): ${error instanceof Error ? error.message : String(error)}`);
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
export {
|
|
180
|
+
formatCausalRetrievalSection,
|
|
181
|
+
retrieveCausalChains
|
|
182
|
+
};
|