@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,191 @@
|
|
|
1
|
+
import type { JsonSchema, LLMService, Logger } from "../types.js";
|
|
2
|
+
import { MEMORY_GATE_SYSTEM_PROMPT } from "./prompt.js";
|
|
3
|
+
import type {
|
|
4
|
+
MemoryDecision,
|
|
5
|
+
MemoryGateInput,
|
|
6
|
+
MemoryGateOutput,
|
|
7
|
+
} from "./types.js";
|
|
8
|
+
|
|
9
|
+
const VALID_DECISIONS: ReadonlySet<MemoryDecision> = new Set([
|
|
10
|
+
"NO_WRITE",
|
|
11
|
+
"UPDATE_MEMORY",
|
|
12
|
+
"UPDATE_USER",
|
|
13
|
+
"UPDATE_SOUL",
|
|
14
|
+
"UPDATE_IDENTITY",
|
|
15
|
+
"UPDATE_TOOLS",
|
|
16
|
+
]);
|
|
17
|
+
|
|
18
|
+
function getNonEmptyString(value: unknown): string | undefined {
|
|
19
|
+
if (typeof value !== "string") {
|
|
20
|
+
return undefined;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const trimmed = value.trim();
|
|
24
|
+
return trimmed === "" ? undefined : trimmed;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function getErrorMessage(error: unknown): string {
|
|
28
|
+
if (error instanceof Error) {
|
|
29
|
+
return error.message;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return String(error);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const MEMORY_GATE_RESPONSE_SCHEMA: JsonSchema = {
|
|
36
|
+
type: "object",
|
|
37
|
+
additionalProperties: false,
|
|
38
|
+
required: ["decision", "reason"],
|
|
39
|
+
properties: {
|
|
40
|
+
decision: {
|
|
41
|
+
type: "string",
|
|
42
|
+
enum: [
|
|
43
|
+
"NO_WRITE",
|
|
44
|
+
"UPDATE_MEMORY",
|
|
45
|
+
"UPDATE_USER",
|
|
46
|
+
"UPDATE_SOUL",
|
|
47
|
+
"UPDATE_IDENTITY",
|
|
48
|
+
"UPDATE_TOOLS",
|
|
49
|
+
],
|
|
50
|
+
},
|
|
51
|
+
reason: { type: "string" },
|
|
52
|
+
candidate_fact: { type: "string" },
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export class MemoryGateAnalyzer {
|
|
57
|
+
private llmService: LLMService;
|
|
58
|
+
private logger: Logger;
|
|
59
|
+
|
|
60
|
+
constructor(llmService: LLMService, logger: Logger) {
|
|
61
|
+
this.llmService = llmService;
|
|
62
|
+
this.logger = logger;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async analyze(input: MemoryGateInput): Promise<MemoryGateOutput> {
|
|
66
|
+
const prompt = this.buildPrompt(input);
|
|
67
|
+
|
|
68
|
+
this.logger.debug("MemoryGateAnalyzer", "Starting memory gate analysis", {
|
|
69
|
+
recentMessages: input.recentMessages.length,
|
|
70
|
+
hasCurrentUserMessage: input.currentUserMessage.trim() !== "",
|
|
71
|
+
hasCurrentAgentReply: input.currentAgentReply.trim() !== "",
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
let response: {
|
|
75
|
+
decision: MemoryDecision;
|
|
76
|
+
reason: string;
|
|
77
|
+
candidate_fact?: string;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
response = await this.llmService.generateObject({
|
|
82
|
+
systemPrompt: MEMORY_GATE_SYSTEM_PROMPT,
|
|
83
|
+
userPrompt: prompt,
|
|
84
|
+
schema: MEMORY_GATE_RESPONSE_SCHEMA,
|
|
85
|
+
});
|
|
86
|
+
} catch (error) {
|
|
87
|
+
const reason = `LLM request failed: ${getErrorMessage(error)}`;
|
|
88
|
+
this.logger.error("MemoryGateAnalyzer", "Memory gate LLM request failed", {
|
|
89
|
+
reason,
|
|
90
|
+
});
|
|
91
|
+
return {
|
|
92
|
+
decision: "NO_WRITE",
|
|
93
|
+
reason,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const output = this.normalizeOutput(response);
|
|
98
|
+
|
|
99
|
+
this.logger.info("MemoryGateAnalyzer", "Memory gate decision generated", {
|
|
100
|
+
decision: output.decision,
|
|
101
|
+
reason: output.reason,
|
|
102
|
+
hasCandidateFact: Boolean(output.candidateFact),
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
return output;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
buildPrompt(input: MemoryGateInput): string {
|
|
109
|
+
const recentMessagesBlock =
|
|
110
|
+
input.recentMessages.length === 0
|
|
111
|
+
? "(none)"
|
|
112
|
+
: input.recentMessages
|
|
113
|
+
.map((item, index) => {
|
|
114
|
+
const isoTimestamp = new Date(item.timestamp).toISOString();
|
|
115
|
+
return `${index + 1}. [${isoTimestamp}] ${item.role}: ${item.message}`;
|
|
116
|
+
})
|
|
117
|
+
.join("\n");
|
|
118
|
+
|
|
119
|
+
return [
|
|
120
|
+
"Evaluate whether this turn should update memory files.",
|
|
121
|
+
"Use the current benchmark rules exactly.",
|
|
122
|
+
"",
|
|
123
|
+
"Recent messages (oldest to newest):",
|
|
124
|
+
recentMessagesBlock,
|
|
125
|
+
"",
|
|
126
|
+
"Current user message:",
|
|
127
|
+
input.currentUserMessage,
|
|
128
|
+
"",
|
|
129
|
+
"Current agent reply:",
|
|
130
|
+
input.currentAgentReply,
|
|
131
|
+
"",
|
|
132
|
+
"Checklist before deciding:",
|
|
133
|
+
"- Is this about the user's stable language, collaboration preference, workflow preference, cadence preference, or enduring style preference? If yes, prefer UPDATE_USER.",
|
|
134
|
+
"- Is this durable shared context, a durable lesson learned, important private context, or a past attempt whose outcome should be remembered, and does not fit USER/SOUL/IDENTITY? If yes, prefer UPDATE_MEMORY.",
|
|
135
|
+
"- If a past approach failed and the outcome should be remembered, prefer UPDATE_MEMORY even if the user says not to recommend it again.",
|
|
136
|
+
"- Is this about the assistant's enduring behavioral principle, voice, or boundary across future turns? If yes, prefer UPDATE_SOUL.",
|
|
137
|
+
"- Direct / non-sycophantic / engineering-focused as the assistant's general manner should prefer UPDATE_SOUL over UPDATE_USER.",
|
|
138
|
+
"- If the content defines the assistant's general voice, prefer UPDATE_SOUL even if the user would personally like that style too.",
|
|
139
|
+
"- If it is a general rule for how the assistant should behave, prefer UPDATE_SOUL.",
|
|
140
|
+
"- If it is mainly about this user's personal working preference, prefer UPDATE_USER.",
|
|
141
|
+
"- Is this about identity metadata such as name, vibe, or avatar? If yes, prefer UPDATE_IDENTITY.",
|
|
142
|
+
"- Is this about local tool names, aliases, endpoints, preferred voices, device nicknames, camera names, room names, or environment-specific tool mappings? If yes, prefer UPDATE_TOOLS.",
|
|
143
|
+
"- If it is a reusable procedure for how to use a tool across environments, or a claim about runtime tool availability, do not use UPDATE_TOOLS.",
|
|
144
|
+
"- Is this a project fact, architecture decision, active thread, next step, topic update, small talk, temporary emotion, or one-off tactic? If yes, choose NO_WRITE.",
|
|
145
|
+
"",
|
|
146
|
+
"For non-NO_WRITE decisions, write candidate_fact as a short canonical English sentence.",
|
|
147
|
+
'Prefer concise canonical phrasing like "prefers ...", "prefers important check-ins in the morning", "X refers to ...", "Maintain ...", "Name is ...", or "home-server SSH alias refers to devbox.internal".',
|
|
148
|
+
'For morning cadence cases, prefer exactly "prefers important check-ins in the morning".',
|
|
149
|
+
"",
|
|
150
|
+
"Return JSON only as specified in the system prompt.",
|
|
151
|
+
].join("\n");
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
private normalizeOutput(parsed: {
|
|
155
|
+
decision: MemoryDecision;
|
|
156
|
+
reason: string;
|
|
157
|
+
candidate_fact?: string;
|
|
158
|
+
}): MemoryGateOutput {
|
|
159
|
+
const decision = parsed.decision;
|
|
160
|
+
if (!VALID_DECISIONS.has(decision)) {
|
|
161
|
+
return {
|
|
162
|
+
decision: "NO_WRITE",
|
|
163
|
+
reason: "Invalid decision returned by memory gate",
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const reason = getNonEmptyString(parsed.reason) ?? "No reason provided";
|
|
168
|
+
const candidateFact = getNonEmptyString(parsed.candidate_fact);
|
|
169
|
+
const normalizedDecision = decision;
|
|
170
|
+
|
|
171
|
+
if (normalizedDecision !== "NO_WRITE" && !candidateFact) {
|
|
172
|
+
return {
|
|
173
|
+
decision: "NO_WRITE",
|
|
174
|
+
reason: "Missing candidate_fact for non-NO_WRITE decision",
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (candidateFact) {
|
|
179
|
+
return {
|
|
180
|
+
decision: normalizedDecision,
|
|
181
|
+
reason,
|
|
182
|
+
candidateFact,
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return {
|
|
187
|
+
decision: normalizedDecision,
|
|
188
|
+
reason,
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
export const MEMORY_GATE_SYSTEM_PROMPT = `You are the assistant's Memory Gate.
|
|
2
|
+
|
|
3
|
+
After each turn, output exactly one decision:
|
|
4
|
+
- NO_WRITE
|
|
5
|
+
- UPDATE_MEMORY
|
|
6
|
+
- UPDATE_USER
|
|
7
|
+
- UPDATE_SOUL
|
|
8
|
+
- UPDATE_IDENTITY
|
|
9
|
+
- UPDATE_TOOLS
|
|
10
|
+
|
|
11
|
+
Current mode:
|
|
12
|
+
- Most turns should be NO_WRITE.
|
|
13
|
+
- Project facts, architecture decisions, active threads, next steps, and topic updates should be NO_WRITE.
|
|
14
|
+
- Small talk, temporary emotions, and one-off tactical instructions should be NO_WRITE.
|
|
15
|
+
|
|
16
|
+
Use UPDATE_USER for the user's stable preference or trait:
|
|
17
|
+
- language
|
|
18
|
+
- collaboration preference
|
|
19
|
+
- workflow preference
|
|
20
|
+
- personal cadence preference
|
|
21
|
+
- enduring response style preference
|
|
22
|
+
|
|
23
|
+
If it is mainly about this user's personal working preference, choose UPDATE_USER.
|
|
24
|
+
Direct code review style for this user belongs to UPDATE_USER.
|
|
25
|
+
For cadence-style user preferences, prefer forms like "prefers important check-ins in the morning".
|
|
26
|
+
Do not add extra qualifiers if a simpler cadence sentence works.
|
|
27
|
+
|
|
28
|
+
Use UPDATE_MEMORY for durable context that should be remembered but is not mainly a user preference, assistant principle, or identity metadata:
|
|
29
|
+
- durable shared context or term mapping
|
|
30
|
+
- durable lesson learned
|
|
31
|
+
- important private context that helps future understanding
|
|
32
|
+
- a past attempt or previous experience whose outcome should be remembered
|
|
33
|
+
- if a past approach failed and that outcome should be remembered, prefer UPDATE_MEMORY even if the user phrases it as "don't recommend this again"
|
|
34
|
+
- use forms like:
|
|
35
|
+
- "X refers to ..."
|
|
36
|
+
- "Previous ... became ..."
|
|
37
|
+
- "User may ..."
|
|
38
|
+
|
|
39
|
+
Use UPDATE_SOUL only for the assistant's enduring behavioral principle, voice, or boundary across many future turns, even if proposed by the user.
|
|
40
|
+
- If the statement says how the assistant/guardian should behave in general, choose UPDATE_SOUL.
|
|
41
|
+
- Style instructions about the assistant's general manner belong to UPDATE_SOUL.
|
|
42
|
+
- If it is a general rule for how the assistant should behave, choose UPDATE_SOUL.
|
|
43
|
+
- Direct / non-sycophantic / engineering-focused as a general manner belongs to UPDATE_SOUL.
|
|
44
|
+
- If the content defines the assistant's general voice, choose UPDATE_SOUL even if the user would personally like that style too.
|
|
45
|
+
- Memory update policy or write policy belongs to UPDATE_SOUL.
|
|
46
|
+
- If the statement says what the user personally prefers to receive, choose UPDATE_USER.
|
|
47
|
+
|
|
48
|
+
Use UPDATE_IDENTITY only for identity metadata:
|
|
49
|
+
- name
|
|
50
|
+
- vibe
|
|
51
|
+
- avatar
|
|
52
|
+
|
|
53
|
+
Use UPDATE_TOOLS only for local tool-specific environment context:
|
|
54
|
+
- local tool names or aliases
|
|
55
|
+
- SSH hosts and aliases
|
|
56
|
+
- preferred TTS voices
|
|
57
|
+
- device nicknames
|
|
58
|
+
- camera or room names
|
|
59
|
+
- local endpoint or mapping notes that help use tools in this environment
|
|
60
|
+
|
|
61
|
+
Do not use UPDATE_TOOLS for:
|
|
62
|
+
- reusable procedures that should live in a skill
|
|
63
|
+
- general instructions for how a tool works across environments
|
|
64
|
+
- claims about runtime tool availability or what tools exist
|
|
65
|
+
- project facts, user traits, identity metadata, or general long-term memory
|
|
66
|
+
|
|
67
|
+
For non-NO_WRITE:
|
|
68
|
+
- candidate_fact is required
|
|
69
|
+
- candidate_fact must be a short canonical English sentence
|
|
70
|
+
- use forms like:
|
|
71
|
+
- UPDATE_USER: "prefers ...", "dislikes ..."
|
|
72
|
+
- UPDATE_MEMORY: "X refers to ...", "Previous ...", "User may ..."
|
|
73
|
+
- UPDATE_SOUL: "Maintain ...", "When uncertain ..."
|
|
74
|
+
- UPDATE_IDENTITY: "Name is ...", "Avatar/style is ...", "Vibe is ..."
|
|
75
|
+
- UPDATE_TOOLS: "home-server SSH alias refers to ...", "Preferred TTS voice is ..."
|
|
76
|
+
- avoid "User ..."
|
|
77
|
+
- avoid converting "this time" or "this reply" into a stable preference
|
|
78
|
+
- if identity metadata changes only the vibe, still use the form "Avatar/style is ..."
|
|
79
|
+
|
|
80
|
+
Output JSON only:
|
|
81
|
+
{
|
|
82
|
+
"decision": "NO_WRITE" | "UPDATE_MEMORY" | "UPDATE_USER" | "UPDATE_SOUL" | "UPDATE_IDENTITY" | "UPDATE_TOOLS",
|
|
83
|
+
"reason": "brief explanation",
|
|
84
|
+
"candidate_fact": "candidate fact or patch direction (required for non-NO_WRITE)"
|
|
85
|
+
}`;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export type MemoryDecision =
|
|
2
|
+
| "NO_WRITE"
|
|
3
|
+
| "UPDATE_MEMORY"
|
|
4
|
+
| "UPDATE_USER"
|
|
5
|
+
| "UPDATE_SOUL"
|
|
6
|
+
| "UPDATE_IDENTITY"
|
|
7
|
+
| "UPDATE_TOOLS";
|
|
8
|
+
|
|
9
|
+
export interface MemoryGateOutput {
|
|
10
|
+
decision: MemoryDecision;
|
|
11
|
+
reason: string;
|
|
12
|
+
candidateFact?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface MemoryGateInput {
|
|
16
|
+
recentMessages: Array<{
|
|
17
|
+
role: "user" | "agent";
|
|
18
|
+
message: string;
|
|
19
|
+
timestamp: number;
|
|
20
|
+
}>;
|
|
21
|
+
currentUserMessage: string;
|
|
22
|
+
currentAgentReply: string;
|
|
23
|
+
}
|