@parkgogogo/openclaw-reflection 0.1.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/INSTALL.md +78 -0
- package/README.md +195 -0
- package/openclaw.plugin.json +67 -0
- package/package.json +52 -0
- package/src/buffer.ts +40 -0
- package/src/config.ts +254 -0
- package/src/consolidation/consolidator.ts +316 -0
- package/src/consolidation/index.ts +9 -0
- package/src/consolidation/prompt.ts +58 -0
- package/src/consolidation/scheduler.ts +153 -0
- package/src/consolidation/types.ts +25 -0
- package/src/evals/cli.ts +45 -0
- package/src/evals/datasets.ts +39 -0
- package/src/evals/runner.ts +446 -0
- package/src/file-curator/index.ts +204 -0
- package/src/index.ts +323 -0
- package/src/llm/index.ts +11 -0
- package/src/llm/service.ts +447 -0
- package/src/llm/types.ts +87 -0
- package/src/logger.ts +125 -0
- package/src/memory-gate/analyzer.ts +191 -0
- package/src/memory-gate/index.ts +7 -0
- package/src/memory-gate/prompt.ts +85 -0
- package/src/memory-gate/types.ts +23 -0
- package/src/message-handler.ts +862 -0
- package/src/proper-lockfile.d.ts +25 -0
- package/src/session-manager.ts +114 -0
- package/src/types.ts +109 -0
- package/src/utils/file-utils.ts +228 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
import * as path from "path";
|
|
2
|
+
import * as url from "url";
|
|
3
|
+
import {
|
|
4
|
+
createConfigLogSnapshot,
|
|
5
|
+
parseConfig,
|
|
6
|
+
resolveWorkspaceDir,
|
|
7
|
+
} from "./config.js";
|
|
8
|
+
import { ConsolidationScheduler } from "./consolidation/index.js";
|
|
9
|
+
import { FileCurator } from "./file-curator/index.js";
|
|
10
|
+
import { LLMService as SharedLLMService } from "./llm/service.js";
|
|
11
|
+
import { FileLogger } from "./logger.js";
|
|
12
|
+
import {
|
|
13
|
+
MemoryGateAnalyzer,
|
|
14
|
+
} from "./memory-gate/index.js";
|
|
15
|
+
import {
|
|
16
|
+
handleBeforeMessageWrite,
|
|
17
|
+
handleMessageReceived,
|
|
18
|
+
} from "./message-handler.js";
|
|
19
|
+
import { SessionBufferManager } from "./session-manager.js";
|
|
20
|
+
import type {
|
|
21
|
+
LLMService,
|
|
22
|
+
PluginConfig,
|
|
23
|
+
} from "./types.js";
|
|
24
|
+
|
|
25
|
+
type LoggerMethod = (message: string, ...args: unknown[]) => void;
|
|
26
|
+
|
|
27
|
+
export interface PluginLogger {
|
|
28
|
+
debug: LoggerMethod;
|
|
29
|
+
info: LoggerMethod;
|
|
30
|
+
warn: LoggerMethod;
|
|
31
|
+
error: LoggerMethod;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface PluginAPI {
|
|
35
|
+
pluginConfig?: unknown;
|
|
36
|
+
config?: {
|
|
37
|
+
get?: (key: string) => unknown;
|
|
38
|
+
};
|
|
39
|
+
logger: PluginLogger;
|
|
40
|
+
registerHook: (
|
|
41
|
+
event: string,
|
|
42
|
+
handler: (event: unknown, context?: unknown) => void,
|
|
43
|
+
options?: { name?: string }
|
|
44
|
+
) => void;
|
|
45
|
+
on?: (
|
|
46
|
+
hookName: string,
|
|
47
|
+
handler: (event: unknown, context?: unknown) => void,
|
|
48
|
+
options?: { priority?: number }
|
|
49
|
+
) => void;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
let bufferManager: SessionBufferManager | null = null;
|
|
53
|
+
let gatewayLogger: PluginLogger | null = null;
|
|
54
|
+
let fileLogger: FileLogger | null = null;
|
|
55
|
+
let isRegistered = false;
|
|
56
|
+
|
|
57
|
+
function getErrorMessage(error: unknown): string {
|
|
58
|
+
if (error instanceof Error) {
|
|
59
|
+
return error.message;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return String(error);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function createLLMService(config: PluginConfig): LLMService {
|
|
66
|
+
const { baseURL, apiKey, model } = config.llm;
|
|
67
|
+
|
|
68
|
+
if (baseURL.trim() === "" || apiKey.trim() === "" || model.trim() === "") {
|
|
69
|
+
throw new Error("LLM config requires non-empty llm.baseURL, llm.apiKey, and llm.model");
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return new SharedLLMService({
|
|
73
|
+
baseURL,
|
|
74
|
+
apiKey,
|
|
75
|
+
model,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function isConfigOnlyModeEnabled(): boolean {
|
|
80
|
+
const value = process.env.OPENCLAW_REFLECTION_CONFIG_ONLY;
|
|
81
|
+
if (typeof value !== "string") {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const normalizedValue = value.trim().toLowerCase();
|
|
86
|
+
return (
|
|
87
|
+
normalizedValue === "1" ||
|
|
88
|
+
normalizedValue === "true" ||
|
|
89
|
+
normalizedValue === "yes" ||
|
|
90
|
+
normalizedValue === "on"
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function runHookSafely(
|
|
95
|
+
logger: FileLogger,
|
|
96
|
+
hookName: string,
|
|
97
|
+
handler: () => void
|
|
98
|
+
): void {
|
|
99
|
+
try {
|
|
100
|
+
handler();
|
|
101
|
+
} catch (error) {
|
|
102
|
+
logger.error("PluginLifecycle", "Hook handler execution failed", {
|
|
103
|
+
hookName,
|
|
104
|
+
reason: getErrorMessage(error),
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function registerMessageHook(
|
|
110
|
+
api: PluginAPI,
|
|
111
|
+
hookName: "message_received",
|
|
112
|
+
handler: (event: unknown, context?: unknown) => unknown
|
|
113
|
+
): void {
|
|
114
|
+
if (typeof api.on === "function") {
|
|
115
|
+
api.on(hookName, handler);
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
api.registerHook("message:received", handler, {
|
|
120
|
+
name: `reflection-${hookName}`,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export default function activate(api: PluginAPI): void {
|
|
125
|
+
if (isRegistered) {
|
|
126
|
+
gatewayLogger?.warn(
|
|
127
|
+
"[Reflection] register called more than once, skipping duplicate registration"
|
|
128
|
+
);
|
|
129
|
+
fileLogger?.warn(
|
|
130
|
+
"PluginLifecycle",
|
|
131
|
+
"Register called more than once, skipping duplicate registration"
|
|
132
|
+
);
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
gatewayLogger = api.logger;
|
|
137
|
+
|
|
138
|
+
try {
|
|
139
|
+
const config: PluginConfig = parseConfig(api);
|
|
140
|
+
const configSnapshot = createConfigLogSnapshot(config);
|
|
141
|
+
const configOnlyMode = isConfigOnlyModeEnabled();
|
|
142
|
+
|
|
143
|
+
const __filename = url.fileURLToPath(import.meta.url);
|
|
144
|
+
const __dirname = path.dirname(__filename);
|
|
145
|
+
const pluginRootDir = path.resolve(__dirname, "..");
|
|
146
|
+
|
|
147
|
+
const logger = new FileLogger(pluginRootDir, config.logLevel);
|
|
148
|
+
fileLogger = logger;
|
|
149
|
+
|
|
150
|
+
gatewayLogger.info("[Reflection] Plugin starting...");
|
|
151
|
+
logger.info("PluginLifecycle", "Plugin starting");
|
|
152
|
+
|
|
153
|
+
gatewayLogger.info(
|
|
154
|
+
"[Reflection] Configuration loaded",
|
|
155
|
+
configSnapshot as Record<string, unknown>
|
|
156
|
+
);
|
|
157
|
+
logger.info(
|
|
158
|
+
"PluginLifecycle",
|
|
159
|
+
"Configuration loaded",
|
|
160
|
+
configSnapshot as Record<string, unknown>
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
gatewayLogger.info("[Reflection] File logger initialized");
|
|
164
|
+
logger.info("PluginLifecycle", "File logger initialized");
|
|
165
|
+
|
|
166
|
+
if (configOnlyMode) {
|
|
167
|
+
gatewayLogger.info(
|
|
168
|
+
"[Reflection] Config-only mode enabled, skipping hooks and side effects",
|
|
169
|
+
configSnapshot as Record<string, unknown>
|
|
170
|
+
);
|
|
171
|
+
logger.info(
|
|
172
|
+
"PluginLifecycle",
|
|
173
|
+
"Config-only mode enabled, skipping hooks and side effects",
|
|
174
|
+
configSnapshot as Record<string, unknown>
|
|
175
|
+
);
|
|
176
|
+
isRegistered = true;
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
bufferManager = new SessionBufferManager(config.bufferSize, logger);
|
|
181
|
+
|
|
182
|
+
gatewayLogger.info("[Reflection] SessionBufferManager initialized");
|
|
183
|
+
logger.info("PluginLifecycle", "SessionBufferManager initialized", {
|
|
184
|
+
bufferSize: config.bufferSize,
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
const workspaceResolution = resolveWorkspaceDir(api);
|
|
188
|
+
const workspaceDir = workspaceResolution.workspaceDir;
|
|
189
|
+
|
|
190
|
+
if (workspaceDir) {
|
|
191
|
+
logger.info("PluginLifecycle", "Workspace resolved", {
|
|
192
|
+
workspaceDir,
|
|
193
|
+
source: workspaceResolution.source,
|
|
194
|
+
});
|
|
195
|
+
} else {
|
|
196
|
+
logger.error("PluginLifecycle", "Workspace unavailable", {
|
|
197
|
+
source: workspaceResolution.source,
|
|
198
|
+
reason: workspaceResolution.reason,
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const llmService =
|
|
203
|
+
config.memoryGate.enabled || config.consolidation.enabled
|
|
204
|
+
? createLLMService(config)
|
|
205
|
+
: undefined;
|
|
206
|
+
|
|
207
|
+
let memoryGate: MemoryGateAnalyzer | undefined;
|
|
208
|
+
let fileCurator: FileCurator | undefined;
|
|
209
|
+
|
|
210
|
+
if (config.memoryGate.enabled && llmService) {
|
|
211
|
+
memoryGate = new MemoryGateAnalyzer(llmService, logger);
|
|
212
|
+
logger.info("PluginLifecycle", "MemoryGateAnalyzer initialized", {
|
|
213
|
+
model: config.llm.model,
|
|
214
|
+
});
|
|
215
|
+
} else {
|
|
216
|
+
logger.info("PluginLifecycle", "MemoryGateAnalyzer disabled");
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (llmService && workspaceDir) {
|
|
220
|
+
fileCurator = new FileCurator({ workspaceDir }, logger, llmService);
|
|
221
|
+
logger.info("PluginLifecycle", "FileCurator initialized", {
|
|
222
|
+
workspaceDir,
|
|
223
|
+
});
|
|
224
|
+
} else if (llmService) {
|
|
225
|
+
logger.warn("PluginLifecycle", "FileCurator disabled: workspace unavailable", {
|
|
226
|
+
source: workspaceResolution.source,
|
|
227
|
+
reason: workspaceResolution.reason,
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (config.consolidation.enabled && llmService && workspaceDir) {
|
|
232
|
+
const consolidationScheduler = new ConsolidationScheduler(
|
|
233
|
+
{
|
|
234
|
+
workspaceDir,
|
|
235
|
+
schedule: config.consolidation.schedule,
|
|
236
|
+
},
|
|
237
|
+
logger,
|
|
238
|
+
llmService
|
|
239
|
+
);
|
|
240
|
+
|
|
241
|
+
consolidationScheduler.start();
|
|
242
|
+
|
|
243
|
+
logger.info(
|
|
244
|
+
"PluginLifecycle",
|
|
245
|
+
"ConsolidationScheduler initialized and started",
|
|
246
|
+
{
|
|
247
|
+
schedule: config.consolidation.schedule,
|
|
248
|
+
}
|
|
249
|
+
);
|
|
250
|
+
} else if (config.consolidation.enabled && llmService) {
|
|
251
|
+
logger.warn("PluginLifecycle", "ConsolidationScheduler disabled: workspace unavailable", {
|
|
252
|
+
source: workspaceResolution.source,
|
|
253
|
+
reason: workspaceResolution.reason,
|
|
254
|
+
});
|
|
255
|
+
} else {
|
|
256
|
+
logger.info("PluginLifecycle", "ConsolidationScheduler disabled");
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (typeof api.on === "function") {
|
|
260
|
+
api.on("before_message_write", (event: unknown, context?: unknown) => {
|
|
261
|
+
runHookSafely(logger, "before_message_write", () => {
|
|
262
|
+
if (bufferManager) {
|
|
263
|
+
handleBeforeMessageWrite(
|
|
264
|
+
event,
|
|
265
|
+
bufferManager,
|
|
266
|
+
logger,
|
|
267
|
+
context,
|
|
268
|
+
memoryGate,
|
|
269
|
+
fileCurator,
|
|
270
|
+
config.memoryGate.windowSize
|
|
271
|
+
);
|
|
272
|
+
} else {
|
|
273
|
+
logger.warn("PluginLifecycle", "Callback skipped: buffer manager missing", {
|
|
274
|
+
hook: "before_message_write",
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
});
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
registerMessageHook(
|
|
282
|
+
api,
|
|
283
|
+
"message_received",
|
|
284
|
+
(event: unknown, context?: unknown) => {
|
|
285
|
+
runHookSafely(logger, "message_received", () => {
|
|
286
|
+
logger.debug("PluginLifecycle", "Callback invoked", {
|
|
287
|
+
hook: "message_received",
|
|
288
|
+
hasContext: context !== undefined,
|
|
289
|
+
hasBufferManager: Boolean(bufferManager),
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
if (bufferManager) {
|
|
293
|
+
handleMessageReceived(event, bufferManager, logger, context);
|
|
294
|
+
logger.debug("PluginLifecycle", "Callback dispatched", {
|
|
295
|
+
hook: "message_received",
|
|
296
|
+
});
|
|
297
|
+
} else {
|
|
298
|
+
logger.warn("PluginLifecycle", "Callback skipped: buffer manager missing", {
|
|
299
|
+
hook: "message_received",
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
);
|
|
305
|
+
|
|
306
|
+
gatewayLogger.info("[Reflection] Message hooks registered");
|
|
307
|
+
logger.info("PluginLifecycle", "Message hooks registered");
|
|
308
|
+
|
|
309
|
+
isRegistered = true;
|
|
310
|
+
gatewayLogger.info(
|
|
311
|
+
"[Reflection] Plugin registered successfully, all hooks active"
|
|
312
|
+
);
|
|
313
|
+
logger.info(
|
|
314
|
+
"PluginLifecycle",
|
|
315
|
+
"Plugin registered successfully, all hooks active"
|
|
316
|
+
);
|
|
317
|
+
} catch (error) {
|
|
318
|
+
const reason = getErrorMessage(error);
|
|
319
|
+
gatewayLogger.error("[Reflection] Plugin activation failed", { reason });
|
|
320
|
+
fileLogger?.error("PluginLifecycle", "Plugin activation failed", { reason });
|
|
321
|
+
throw error;
|
|
322
|
+
}
|
|
323
|
+
}
|