@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
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
declare module "proper-lockfile" {
|
|
2
|
+
export interface RetryOptions {
|
|
3
|
+
retries?: number;
|
|
4
|
+
factor?: number;
|
|
5
|
+
minTimeout?: number;
|
|
6
|
+
maxTimeout?: number;
|
|
7
|
+
randomize?: boolean;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface LockOptions {
|
|
11
|
+
retries?: number | RetryOptions;
|
|
12
|
+
stale?: number;
|
|
13
|
+
realpath?: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export type Release = () => Promise<void>;
|
|
17
|
+
|
|
18
|
+
export function lock(filePath: string, options?: LockOptions): Promise<Release>;
|
|
19
|
+
|
|
20
|
+
const lockfile: {
|
|
21
|
+
lock: typeof lock;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export default lockfile;
|
|
25
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { CircularBuffer } from "./buffer.js";
|
|
2
|
+
import type { Logger, ReflectionMessage } from "./types.js";
|
|
3
|
+
|
|
4
|
+
interface SessionData {
|
|
5
|
+
buffer: CircularBuffer<ReflectionMessage>;
|
|
6
|
+
lastAccessed: number;
|
|
7
|
+
processedAgentMessageIds: Set<string>;
|
|
8
|
+
pendingTask: Promise<void>;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export class SessionBufferManager {
|
|
12
|
+
private sessions: Map<string, SessionData>;
|
|
13
|
+
private capacity: number;
|
|
14
|
+
private logger: Logger;
|
|
15
|
+
|
|
16
|
+
constructor(capacity: number, logger: Logger) {
|
|
17
|
+
this.capacity = capacity;
|
|
18
|
+
this.logger = logger;
|
|
19
|
+
this.sessions = new Map();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
private getOrCreateSessionData(sessionKey: string): SessionData {
|
|
23
|
+
let sessionData = this.sessions.get(sessionKey);
|
|
24
|
+
|
|
25
|
+
if (!sessionData) {
|
|
26
|
+
this.logger.info(
|
|
27
|
+
"SessionBufferManager",
|
|
28
|
+
"Creating new session buffer",
|
|
29
|
+
{ sessionKey },
|
|
30
|
+
sessionKey
|
|
31
|
+
);
|
|
32
|
+
sessionData = {
|
|
33
|
+
buffer: new CircularBuffer<ReflectionMessage>(this.capacity),
|
|
34
|
+
lastAccessed: Date.now(),
|
|
35
|
+
processedAgentMessageIds: new Set<string>(),
|
|
36
|
+
pendingTask: Promise.resolve(),
|
|
37
|
+
};
|
|
38
|
+
this.sessions.set(sessionKey, sessionData);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
sessionData.lastAccessed = Date.now();
|
|
42
|
+
return sessionData;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
push(sessionKey: string, message: ReflectionMessage): void {
|
|
46
|
+
const sessionData = this.getOrCreateSessionData(sessionKey);
|
|
47
|
+
|
|
48
|
+
const evicted = sessionData.buffer.push(message);
|
|
49
|
+
if (evicted) {
|
|
50
|
+
this.logger.info(
|
|
51
|
+
"SessionBufferManager",
|
|
52
|
+
"Evicted oldest message",
|
|
53
|
+
{ evictedId: evicted.id },
|
|
54
|
+
sessionKey
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
sessionData.lastAccessed = Date.now();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
getMessages(sessionKey: string): ReflectionMessage[] {
|
|
62
|
+
const sessionData = this.sessions.get(sessionKey);
|
|
63
|
+
|
|
64
|
+
if (!sessionData) {
|
|
65
|
+
return [];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
sessionData.lastAccessed = Date.now();
|
|
69
|
+
return sessionData.buffer.toArray();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
hasProcessedAgentMessage(sessionKey: string, messageId: string): boolean {
|
|
73
|
+
const sessionData = this.sessions.get(sessionKey);
|
|
74
|
+
if (!sessionData) {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
sessionData.lastAccessed = Date.now();
|
|
79
|
+
return sessionData.processedAgentMessageIds.has(messageId);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
markProcessedAgentMessage(sessionKey: string, messageId: string): void {
|
|
83
|
+
const sessionData = this.getOrCreateSessionData(sessionKey);
|
|
84
|
+
sessionData.processedAgentMessageIds.add(messageId);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
runExclusive(sessionKey: string, task: () => Promise<void>): Promise<void> {
|
|
88
|
+
const sessionData = this.getOrCreateSessionData(sessionKey);
|
|
89
|
+
const nextTask = sessionData.pendingTask
|
|
90
|
+
.catch(() => undefined)
|
|
91
|
+
.then(task);
|
|
92
|
+
|
|
93
|
+
sessionData.pendingTask = nextTask.catch(() => undefined);
|
|
94
|
+
return nextTask;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
clearSession(sessionKey: string): void {
|
|
98
|
+
const existed = this.sessions.has(sessionKey);
|
|
99
|
+
this.sessions.delete(sessionKey);
|
|
100
|
+
|
|
101
|
+
if (existed) {
|
|
102
|
+
this.logger.info(
|
|
103
|
+
"SessionBufferManager",
|
|
104
|
+
"Session cleared",
|
|
105
|
+
{ sessionKey },
|
|
106
|
+
sessionKey
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
getSessionCount(): number {
|
|
112
|
+
return this.sessions.size;
|
|
113
|
+
}
|
|
114
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
export type LogLevel = "debug" | "info" | "warn" | "error";
|
|
2
|
+
|
|
3
|
+
export interface Logger {
|
|
4
|
+
debug(
|
|
5
|
+
component: string,
|
|
6
|
+
event: string,
|
|
7
|
+
details?: Record<string, unknown>,
|
|
8
|
+
sessionKey?: string
|
|
9
|
+
): void;
|
|
10
|
+
info(
|
|
11
|
+
component: string,
|
|
12
|
+
event: string,
|
|
13
|
+
details?: Record<string, unknown>,
|
|
14
|
+
sessionKey?: string
|
|
15
|
+
): void;
|
|
16
|
+
warn(
|
|
17
|
+
component: string,
|
|
18
|
+
event: string,
|
|
19
|
+
details?: Record<string, unknown>,
|
|
20
|
+
sessionKey?: string
|
|
21
|
+
): void;
|
|
22
|
+
error(
|
|
23
|
+
component: string,
|
|
24
|
+
event: string,
|
|
25
|
+
details?: Record<string, unknown>,
|
|
26
|
+
sessionKey?: string
|
|
27
|
+
): void;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface ReflectionMessage {
|
|
31
|
+
id: string;
|
|
32
|
+
role: "user" | "agent";
|
|
33
|
+
message: string;
|
|
34
|
+
timestamp: number;
|
|
35
|
+
sessionKey: string;
|
|
36
|
+
channelId: string;
|
|
37
|
+
metadata?: {
|
|
38
|
+
from?: string;
|
|
39
|
+
to?: string;
|
|
40
|
+
messageId?: string;
|
|
41
|
+
success?: boolean;
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface LogEntry {
|
|
46
|
+
timestamp: string;
|
|
47
|
+
level: LogLevel;
|
|
48
|
+
component: string;
|
|
49
|
+
sessionKey?: string;
|
|
50
|
+
event: string;
|
|
51
|
+
details?: Record<string, unknown>;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export type MemoryDecision =
|
|
55
|
+
| "NO_WRITE"
|
|
56
|
+
| "UPDATE_MEMORY"
|
|
57
|
+
| "UPDATE_USER"
|
|
58
|
+
| "UPDATE_SOUL"
|
|
59
|
+
| "UPDATE_IDENTITY"
|
|
60
|
+
| "UPDATE_TOOLS";
|
|
61
|
+
|
|
62
|
+
export interface MemoryGateOutput {
|
|
63
|
+
decision: MemoryDecision;
|
|
64
|
+
reason: string;
|
|
65
|
+
candidateFact?: string;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface MemoryGateConfig {
|
|
69
|
+
enabled: boolean;
|
|
70
|
+
windowSize: number;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export interface ConsolidationConfig {
|
|
74
|
+
enabled: boolean;
|
|
75
|
+
schedule: string;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export interface LLMConfig {
|
|
79
|
+
baseURL: string;
|
|
80
|
+
apiKey: string;
|
|
81
|
+
model: string;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export interface PluginConfig {
|
|
85
|
+
bufferSize: number;
|
|
86
|
+
logLevel: LogLevel;
|
|
87
|
+
llm: LLMConfig;
|
|
88
|
+
memoryGate: MemoryGateConfig;
|
|
89
|
+
consolidation: ConsolidationConfig;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export type { MemoryGateInput } from "./memory-gate/types.js";
|
|
93
|
+
export type {
|
|
94
|
+
AgentRunResult,
|
|
95
|
+
AgentStep,
|
|
96
|
+
AgentTool,
|
|
97
|
+
GenerateObjectParams,
|
|
98
|
+
JsonSchema,
|
|
99
|
+
LLMService,
|
|
100
|
+
LLMServiceConfig,
|
|
101
|
+
LLMServiceOptions,
|
|
102
|
+
RunAgentParams,
|
|
103
|
+
} from "./llm/types.js";
|
|
104
|
+
export type {
|
|
105
|
+
ConsolidatedFilename,
|
|
106
|
+
ConsolidationPatch,
|
|
107
|
+
ConsolidationProposal,
|
|
108
|
+
ConsolidationResult,
|
|
109
|
+
} from "./consolidation/types.js";
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import * as fs from "fs/promises";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
import lockfile from "proper-lockfile";
|
|
4
|
+
|
|
5
|
+
const LOCK_TIMEOUT_MS = 5000;
|
|
6
|
+
|
|
7
|
+
function getErrorMessage(error: unknown): string {
|
|
8
|
+
if (error instanceof Error) {
|
|
9
|
+
return error.message;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
return String(error);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function isNodeError(error: unknown): error is NodeJS.ErrnoException {
|
|
16
|
+
return typeof error === "object" && error !== null && "code" in error;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function formatDate(date: Date): string {
|
|
20
|
+
const year = date.getFullYear();
|
|
21
|
+
const month = String(date.getMonth() + 1).padStart(2, "0");
|
|
22
|
+
const day = String(date.getDate()).padStart(2, "0");
|
|
23
|
+
return `${year}-${month}-${day}.md`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async function createFileIfMissing(filePath: string): Promise<void> {
|
|
27
|
+
try {
|
|
28
|
+
await fs.access(filePath);
|
|
29
|
+
} catch (error) {
|
|
30
|
+
if (isNodeError(error) && error.code === "ENOENT") {
|
|
31
|
+
const handle = await fs.open(filePath, "a");
|
|
32
|
+
await handle.close();
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
throw error;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function lockForWrite(filePath: string): Promise<() => Promise<void>> {
|
|
41
|
+
try {
|
|
42
|
+
await ensureDir(path.dirname(filePath));
|
|
43
|
+
await createFileIfMissing(filePath);
|
|
44
|
+
|
|
45
|
+
return await lockfile.lock(filePath, {
|
|
46
|
+
retries: {
|
|
47
|
+
retries: 5,
|
|
48
|
+
factor: 1,
|
|
49
|
+
minTimeout: 1000,
|
|
50
|
+
maxTimeout: 1000,
|
|
51
|
+
randomize: false,
|
|
52
|
+
},
|
|
53
|
+
stale: LOCK_TIMEOUT_MS,
|
|
54
|
+
realpath: false,
|
|
55
|
+
});
|
|
56
|
+
} catch (error) {
|
|
57
|
+
throw new Error(
|
|
58
|
+
`Failed to acquire lock for "${filePath}" within ${LOCK_TIMEOUT_MS}ms: ${getErrorMessage(error)}`
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function releaseLock(
|
|
64
|
+
release: () => Promise<void>,
|
|
65
|
+
filePath: string
|
|
66
|
+
): Promise<void> {
|
|
67
|
+
try {
|
|
68
|
+
await release();
|
|
69
|
+
} catch (error) {
|
|
70
|
+
throw new Error(
|
|
71
|
+
`Failed to release lock for "${filePath}": ${getErrorMessage(error)}`
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export async function ensureDir(dirPath: string): Promise<void> {
|
|
77
|
+
try {
|
|
78
|
+
await fs.mkdir(dirPath, { recursive: true });
|
|
79
|
+
} catch (error) {
|
|
80
|
+
throw new Error(`Failed to ensure directory "${dirPath}": ${getErrorMessage(error)}`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export async function readFile(filePath: string): Promise<string | null> {
|
|
85
|
+
try {
|
|
86
|
+
return await fs.readFile(filePath, "utf8");
|
|
87
|
+
} catch (error) {
|
|
88
|
+
if (isNodeError(error) && error.code === "ENOENT") {
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
throw new Error(`Failed to read file "${filePath}": ${getErrorMessage(error)}`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export async function writeFile(filePath: string, content: string): Promise<void> {
|
|
97
|
+
try {
|
|
98
|
+
await ensureDir(path.dirname(filePath));
|
|
99
|
+
await fs.writeFile(filePath, content, "utf8");
|
|
100
|
+
} catch (error) {
|
|
101
|
+
throw new Error(`Failed to write file "${filePath}": ${getErrorMessage(error)}`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export async function writeFileWithLock(
|
|
106
|
+
filePath: string,
|
|
107
|
+
content: string
|
|
108
|
+
): Promise<void> {
|
|
109
|
+
const release = await lockForWrite(filePath);
|
|
110
|
+
let writeError: unknown;
|
|
111
|
+
|
|
112
|
+
try {
|
|
113
|
+
await writeFile(filePath, content);
|
|
114
|
+
} catch (error) {
|
|
115
|
+
writeError = error;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
try {
|
|
119
|
+
await releaseLock(release, filePath);
|
|
120
|
+
} catch (releaseError) {
|
|
121
|
+
if (writeError) {
|
|
122
|
+
throw new Error(
|
|
123
|
+
`Write and release failed for "${filePath}": write=${getErrorMessage(writeError)}; release=${getErrorMessage(releaseError)}`
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
throw releaseError;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (writeError) {
|
|
131
|
+
throw new Error(
|
|
132
|
+
`Failed to write file with lock "${filePath}": ${getErrorMessage(writeError)}`
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export async function appendFile(filePath: string, content: string): Promise<void> {
|
|
138
|
+
try {
|
|
139
|
+
await ensureDir(path.dirname(filePath));
|
|
140
|
+
await fs.appendFile(filePath, content, "utf8");
|
|
141
|
+
} catch (error) {
|
|
142
|
+
throw new Error(`Failed to append file "${filePath}": ${getErrorMessage(error)}`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export async function appendFileWithLock(
|
|
147
|
+
filePath: string,
|
|
148
|
+
content: string
|
|
149
|
+
): Promise<void> {
|
|
150
|
+
const release = await lockForWrite(filePath);
|
|
151
|
+
let appendError: unknown;
|
|
152
|
+
|
|
153
|
+
try {
|
|
154
|
+
await appendFile(filePath, content);
|
|
155
|
+
} catch (error) {
|
|
156
|
+
appendError = error;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
try {
|
|
160
|
+
await releaseLock(release, filePath);
|
|
161
|
+
} catch (releaseError) {
|
|
162
|
+
if (appendError) {
|
|
163
|
+
throw new Error(
|
|
164
|
+
`Append and release failed for "${filePath}": append=${getErrorMessage(appendError)}; release=${getErrorMessage(releaseError)}`
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
throw releaseError;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (appendError) {
|
|
172
|
+
throw new Error(
|
|
173
|
+
`Failed to append file with lock "${filePath}": ${getErrorMessage(appendError)}`
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export async function listFiles(dirPath: string): Promise<string[]> {
|
|
179
|
+
try {
|
|
180
|
+
const entries = await fs.readdir(dirPath, { withFileTypes: true });
|
|
181
|
+
|
|
182
|
+
return entries
|
|
183
|
+
.filter((entry) => entry.isFile())
|
|
184
|
+
.map((entry) => entry.name)
|
|
185
|
+
.sort();
|
|
186
|
+
} catch (error) {
|
|
187
|
+
if (isNodeError(error) && error.code === "ENOENT") {
|
|
188
|
+
return [];
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
throw new Error(
|
|
192
|
+
`Failed to list files in "${dirPath}": ${getErrorMessage(error)}`
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export async function moveFile(fromPath: string, toPath: string): Promise<void> {
|
|
198
|
+
try {
|
|
199
|
+
await ensureDir(path.dirname(toPath));
|
|
200
|
+
await fs.rename(fromPath, toPath);
|
|
201
|
+
} catch (error) {
|
|
202
|
+
if (isNodeError(error) && error.code === "EXDEV") {
|
|
203
|
+
try {
|
|
204
|
+
await fs.copyFile(fromPath, toPath);
|
|
205
|
+
await fs.unlink(fromPath);
|
|
206
|
+
return;
|
|
207
|
+
} catch (copyError) {
|
|
208
|
+
throw new Error(
|
|
209
|
+
`Failed to move file "${fromPath}" to "${toPath}": ${getErrorMessage(copyError)}`
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
throw new Error(
|
|
215
|
+
`Failed to move file "${fromPath}" to "${toPath}": ${getErrorMessage(error)}`
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export function getTodayFilename(): string {
|
|
221
|
+
return formatDate(new Date());
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export function getYesterdayFilename(): string {
|
|
225
|
+
const date = new Date();
|
|
226
|
+
date.setDate(date.getDate() - 1);
|
|
227
|
+
return formatDate(date);
|
|
228
|
+
}
|