pacs-core 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/LICENSE +21 -0
- package/README.md +602 -0
- package/bin/pacs-cli.js +7 -0
- package/package.json +31 -0
- package/phase2-test.mjs +53 -0
- package/src/agent-creator.js +178 -0
- package/src/agent-registry.js +165 -0
- package/src/bootstrap.js +245 -0
- package/src/cli.js +384 -0
- package/src/crypto.js +118 -0
- package/src/index.js +74 -0
- package/src/inter-agent-bus.js +187 -0
- package/src/learn-mode.js +97 -0
- package/src/learning-loop.js +254 -0
- package/src/memory-store.js +226 -0
- package/src/openclaw-bridge.cjs +214 -0
- package/src/openclaw-bridge.js +304 -0
- package/src/routing-engine.js +221 -0
- package/src/test.js +298 -0
- package/src/trigger-parser.js +187 -0
- package/src/triggers.json +122 -0
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PACS Core — Inter-Agent Bus Module
|
|
3
|
+
* Message passing between agents with encrypted persistence
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import crypto from './crypto.js';
|
|
7
|
+
import memoryStore from './memory-store.js';
|
|
8
|
+
|
|
9
|
+
const MESSAGES_FILE = 'messages.enc';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Generate a short unique message ID.
|
|
13
|
+
* @returns {string}
|
|
14
|
+
*/
|
|
15
|
+
function generateMsgId() {
|
|
16
|
+
return Date.now().toString(36) + Math.random().toString(36).slice(2, 7);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Read messages store.
|
|
21
|
+
* @returns {object}
|
|
22
|
+
*/
|
|
23
|
+
function readMessagesStore() {
|
|
24
|
+
try {
|
|
25
|
+
return memoryStore.read(MESSAGES_FILE) || { messages: [], pendingRequests: {} };
|
|
26
|
+
} catch {
|
|
27
|
+
return { messages: [], pendingRequests: {} };
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Write messages store.
|
|
33
|
+
* @param {object} store
|
|
34
|
+
*/
|
|
35
|
+
function writeMessagesStore(store) {
|
|
36
|
+
memoryStore.write(MESSAGES_FILE, store);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Send a message from one agent to another.
|
|
41
|
+
* @param {string} fromAgent - Sender name
|
|
42
|
+
* @param {string} toAgent - Recipient name (or '*' for broadcast)
|
|
43
|
+
* @param {string|object} message - Message content
|
|
44
|
+
* @returns {object} The stored message
|
|
45
|
+
*/
|
|
46
|
+
export function emit(fromAgent, toAgent, message) {
|
|
47
|
+
const store = readMessagesStore();
|
|
48
|
+
const msg = {
|
|
49
|
+
id: generateMsgId(),
|
|
50
|
+
from: fromAgent,
|
|
51
|
+
to: toAgent,
|
|
52
|
+
type: 'message',
|
|
53
|
+
payload: typeof message === 'string' ? message : JSON.stringify(message),
|
|
54
|
+
timestamp: new Date().toISOString(),
|
|
55
|
+
};
|
|
56
|
+
store.messages.push(msg);
|
|
57
|
+
writeMessagesStore(store);
|
|
58
|
+
return msg;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Broadcast a message to all agents.
|
|
63
|
+
* @param {string} fromAgent - Sender name
|
|
64
|
+
* @param {string|object} message - Message content
|
|
65
|
+
* @returns {object} The stored broadcast message
|
|
66
|
+
*/
|
|
67
|
+
export function broadcast(fromAgent, message) {
|
|
68
|
+
return emit(fromAgent, '*', message);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Send a request that expects a response.
|
|
73
|
+
* Returns a promise that resolves with the response.
|
|
74
|
+
* @param {string} fromAgent - Sender name
|
|
75
|
+
* @param {string} toAgent - Recipient name
|
|
76
|
+
* @param {string|object} question - Request payload
|
|
77
|
+
* @param {number} [timeoutMs=30000] - Timeout in milliseconds
|
|
78
|
+
* @returns {Promise<object>} Resolves to the response message
|
|
79
|
+
*/
|
|
80
|
+
export function request(fromAgent, toAgent, question, timeoutMs = 30000) {
|
|
81
|
+
return new Promise((resolve, reject) => {
|
|
82
|
+
const store = readMessagesStore();
|
|
83
|
+
const requestId = generateMsgId();
|
|
84
|
+
|
|
85
|
+
const msg = {
|
|
86
|
+
id: requestId,
|
|
87
|
+
from: fromAgent,
|
|
88
|
+
to: toAgent,
|
|
89
|
+
type: 'request',
|
|
90
|
+
payload: typeof question === 'string' ? question : JSON.stringify(question),
|
|
91
|
+
timestamp: new Date().toISOString(),
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
// Track pending request
|
|
95
|
+
store.pendingRequests[requestId] = { resolve, reject, expires: Date.now() + timeoutMs };
|
|
96
|
+
store.messages.push(msg);
|
|
97
|
+
writeMessagesStore(store);
|
|
98
|
+
|
|
99
|
+
// Timeout
|
|
100
|
+
setTimeout(() => {
|
|
101
|
+
const s = readMessagesStore();
|
|
102
|
+
if (s.pendingRequests[requestId]) {
|
|
103
|
+
delete s.pendingRequests[requestId];
|
|
104
|
+
writeMessagesStore(s);
|
|
105
|
+
reject(new Error(`Request timeout after ${timeoutMs}ms`));
|
|
106
|
+
}
|
|
107
|
+
}, timeoutMs);
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Respond to a request.
|
|
113
|
+
* @param {string} requestId - Original request ID
|
|
114
|
+
* @param {string} fromAgent - Responder name
|
|
115
|
+
* @param {string|object} response - Response payload
|
|
116
|
+
* @returns {object|null} The response message or null if request not found
|
|
117
|
+
*/
|
|
118
|
+
export function respond(requestId, fromAgent, response) {
|
|
119
|
+
const store = readMessagesStore();
|
|
120
|
+
|
|
121
|
+
if (!store.pendingRequests[requestId]) {
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const msg = {
|
|
126
|
+
id: generateMsgId(),
|
|
127
|
+
from: fromAgent,
|
|
128
|
+
to: store.messages.find((m) => m.id === requestId)?.from || 'unknown',
|
|
129
|
+
type: 'response',
|
|
130
|
+
inReplyTo: requestId,
|
|
131
|
+
payload: typeof response === 'string' ? response : JSON.stringify(response),
|
|
132
|
+
timestamp: new Date().toISOString(),
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
store.messages.push(msg);
|
|
136
|
+
|
|
137
|
+
const pending = store.pendingRequests[requestId];
|
|
138
|
+
pending.resolve(msg);
|
|
139
|
+
delete store.pendingRequests[requestId];
|
|
140
|
+
writeMessagesStore(store);
|
|
141
|
+
|
|
142
|
+
return msg;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Get messages for a specific agent.
|
|
147
|
+
* @param {string} agentName
|
|
148
|
+
* @param {object} [options] - { since, type, limit }
|
|
149
|
+
* @returns {object[]}
|
|
150
|
+
*/
|
|
151
|
+
export function getMessages(agentName, options = {}) {
|
|
152
|
+
const store = readMessagesStore();
|
|
153
|
+
let msgs = store.messages.filter(
|
|
154
|
+
(m) => m.to === agentName || m.to === '*' || m.from === agentName
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
if (options.since) {
|
|
158
|
+
msgs = msgs.filter((m) => new Date(m.timestamp) >= new Date(options.since));
|
|
159
|
+
}
|
|
160
|
+
if (options.type) {
|
|
161
|
+
msgs = msgs.filter((m) => m.type === options.type);
|
|
162
|
+
}
|
|
163
|
+
if (options.limit) {
|
|
164
|
+
msgs = msgs.slice(-options.limit);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return msgs;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Clear old messages (keep last N messages per agent pair).
|
|
172
|
+
* @param {number} [keepLast=1000]
|
|
173
|
+
*/
|
|
174
|
+
export function pruneMessages(keepLast = 1000) {
|
|
175
|
+
const store = readMessagesStore();
|
|
176
|
+
store.messages = store.messages.slice(-keepLast);
|
|
177
|
+
writeMessagesStore(store);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export default {
|
|
181
|
+
emit,
|
|
182
|
+
broadcast,
|
|
183
|
+
request,
|
|
184
|
+
respond,
|
|
185
|
+
getMessages,
|
|
186
|
+
pruneMessages,
|
|
187
|
+
};
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PACS Core — Learn Mode Manager
|
|
3
|
+
* Manages the learn mode setting: "safe" | "explicit" | "auto"
|
|
4
|
+
* Settings stored in ~/.openclaw/pacs/settings.enc
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import fs from 'fs';
|
|
8
|
+
import path from 'path';
|
|
9
|
+
import { fileURLToPath } from 'url';
|
|
10
|
+
import crypto from './crypto.js';
|
|
11
|
+
import memoryStore from './memory-store.js';
|
|
12
|
+
|
|
13
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
14
|
+
const SETTINGS_FILE = 'settings.enc';
|
|
15
|
+
const DEFAULT_MODE = 'safe';
|
|
16
|
+
|
|
17
|
+
const VALID_MODES = ['safe', 'explicit', 'auto'];
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Get the settings file path.
|
|
21
|
+
* @returns {string}
|
|
22
|
+
*/
|
|
23
|
+
function getSettingsPath() {
|
|
24
|
+
return memoryStore.getFilePath(SETTINGS_FILE);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Read the settings file.
|
|
29
|
+
* @returns {object}
|
|
30
|
+
*/
|
|
31
|
+
function readSettings() {
|
|
32
|
+
const filePath = getSettingsPath();
|
|
33
|
+
if (!fs.existsSync(filePath)) {
|
|
34
|
+
return { learnMode: DEFAULT_MODE };
|
|
35
|
+
}
|
|
36
|
+
try {
|
|
37
|
+
const encrypted = fs.readFileSync(filePath, 'utf8');
|
|
38
|
+
const decrypted = crypto.decrypt(encrypted);
|
|
39
|
+
return JSON.parse(decrypted);
|
|
40
|
+
} catch {
|
|
41
|
+
return { learnMode: DEFAULT_MODE };
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Write the settings file.
|
|
47
|
+
* @param {object} settings
|
|
48
|
+
*/
|
|
49
|
+
function writeSettings(settings) {
|
|
50
|
+
memoryStore.ensureStorageDir
|
|
51
|
+
? memoryStore.ensureStorageDir()
|
|
52
|
+
: void 0;
|
|
53
|
+
const filePath = getSettingsPath();
|
|
54
|
+
const json = JSON.stringify(settings, null, 2);
|
|
55
|
+
const encrypted = crypto.encrypt(json);
|
|
56
|
+
fs.writeFileSync(filePath, encrypted, 'utf8');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Get the current learn mode.
|
|
61
|
+
* @returns {string} "safe" | "explicit" | "auto"
|
|
62
|
+
*/
|
|
63
|
+
export function getMode() {
|
|
64
|
+
const settings = readSettings();
|
|
65
|
+
return VALID_MODES.includes(settings.learnMode) ? settings.learnMode : DEFAULT_MODE;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Set the learn mode.
|
|
70
|
+
* @param {string} mode - "safe" | "explicit" | "auto"
|
|
71
|
+
* @returns {string} The mode that was set
|
|
72
|
+
*/
|
|
73
|
+
export function setMode(mode) {
|
|
74
|
+
if (!VALID_MODES.includes(mode)) {
|
|
75
|
+
throw new Error(`Invalid learn mode: ${mode}. Must be one of: ${VALID_MODES.join(', ')}`);
|
|
76
|
+
}
|
|
77
|
+
const settings = readSettings();
|
|
78
|
+
settings.learnMode = mode;
|
|
79
|
+
writeSettings(settings);
|
|
80
|
+
return mode;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Get a human-readable description of the current mode.
|
|
85
|
+
* @returns {string}
|
|
86
|
+
*/
|
|
87
|
+
export function getModeDescription() {
|
|
88
|
+
const mode = getMode();
|
|
89
|
+
const descriptions = {
|
|
90
|
+
safe: 'AI asks before storing anything ("Soll ich mir das merken?")',
|
|
91
|
+
explicit: 'AI only learns when explicitly commanded via MERKE:/LEARN:',
|
|
92
|
+
auto: 'AI auto-confirms after storing and reminds you what was learned',
|
|
93
|
+
};
|
|
94
|
+
return descriptions[mode] || descriptions.safe;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export default { getMode, setMode, getModeDescription };
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PACS Core — Learning Loop
|
|
3
|
+
* Handles SAFE learning mode: "Soll ich mir das merken?"
|
|
4
|
+
* Supports: "safe" (ask), "explicit" (only on command), "auto" (confirm after)
|
|
5
|
+
* Detection uses trigger-parser patterns + LLM inference patterns
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import triggerParser from './trigger-parser.js';
|
|
9
|
+
import memoryStore from './memory-store.js';
|
|
10
|
+
import learnMode from './learn-mode.js';
|
|
11
|
+
|
|
12
|
+
// Triggers that signal something worth remembering
|
|
13
|
+
const REMEMBER_TRIGGERS = [
|
|
14
|
+
/ich\s+(?:heiße|heiße|bin\s+[^.]+)|my\s+name\s+is/i,
|
|
15
|
+
/ich\s+(?:wohne|live|lebe|in)/i,
|
|
16
|
+
/ich\s+(?:arbeite|arbeite\s+bei|work\s+(?:at|for|as))/i,
|
|
17
|
+
/ich\s+(?:mag|like|preferiere|favorisiere)/i,
|
|
18
|
+
/ich\s+(?:hasse|dislike)/i,
|
|
19
|
+
/ich\s+(?:bin|am|im).*(?:gerade|currently|now)/i,
|
|
20
|
+
/^\s*(?:bisher|so\s+far|erfahrung|experience).*:?\s*/i,
|
|
21
|
+
/^\s*(?:präferenz|preference|favorite|liebling)/i,
|
|
22
|
+
/^\s*(?:wunsch|wish|want|would\s+like)/i,
|
|
23
|
+
/^\s*(?:merke|note|notiz|remember).*:?\s*/i,
|
|
24
|
+
/^\s*(?:info|information|fyi)/i,
|
|
25
|
+
/^\s*(?:das\s+ist|that\s+is|this\s+is).*(?:mein|my|mine)/i,
|
|
26
|
+
/^\s*(?:über\s+mich|about\s+me|about\s+me)/i,
|
|
27
|
+
/^\s*(?:hintergrund|background|context)/i,
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
// Patterns that indicate explicit forget request
|
|
31
|
+
const FORGET_TRIGGERS = [
|
|
32
|
+
/^(?:vergiss|forget|lösche|delete|entferne).*:?\s*/i,
|
|
33
|
+
/^(?:nicht\s+(?:merken|erinnern)|don't\s+remember)/i,
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
// Patterns that suppress learning (noise, not facts)
|
|
37
|
+
const NOISE_PATTERNS = [
|
|
38
|
+
/^(?:thanks?|danke|ok|okay|ja|yes|no|nein|yep|nope)\s*$/i,
|
|
39
|
+
/^(?:hi|hello|hey|greetings|hallo)\s*$/i,
|
|
40
|
+
/^(?:bye|goodbye|ttafn|see\s+you)\s*$/i,
|
|
41
|
+
/^(?:how\s+are\s+you|wie\s+geht)/i,
|
|
42
|
+
/^(?:what(?:'s|\s+is)\s+up|was\s+geht)/i,
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
// Question patterns — generally don't learn from questions
|
|
46
|
+
const QUESTION_PATTERNS = [
|
|
47
|
+
/^\s*(?:was|wie|wer|wo|warum|weshalb|welche|r)/i,
|
|
48
|
+
/^\s*(?:what|how|who|where|why|which|when)/i,
|
|
49
|
+
/^\s*(?:can|could|would|should|will|is|are|do|does)\s+/i,
|
|
50
|
+
/[?!]\s*$/,
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Check if a message is likely noise (not a fact worth learning).
|
|
55
|
+
* @param {string} message
|
|
56
|
+
* @returns {boolean}
|
|
57
|
+
*/
|
|
58
|
+
function isNoise(message) {
|
|
59
|
+
if (!message || typeof message !== 'string') return true;
|
|
60
|
+
const trimmed = message.trim();
|
|
61
|
+
if (trimmed.length < 3) return true;
|
|
62
|
+
return NOISE_PATTERNS.some((p) => p.test(trimmed));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Check if a message is a question (typically not learnable).
|
|
67
|
+
* @param {string} message
|
|
68
|
+
* @returns {boolean}
|
|
69
|
+
*/
|
|
70
|
+
function isQuestion(message) {
|
|
71
|
+
if (!message || typeof message !== 'string') return false;
|
|
72
|
+
return QUESTION_PATTERNS.some((p) => p.test(message.trim()));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Detect if a message is an explicit forget request.
|
|
77
|
+
* @param {string} message
|
|
78
|
+
* @returns {boolean}
|
|
79
|
+
*/
|
|
80
|
+
function isForgetRequest(message) {
|
|
81
|
+
if (!message || typeof message !== 'string') return false;
|
|
82
|
+
return FORGET_TRIGGERS.some((p) => p.test(message.trim()));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Detect if a message looks like a personal fact statement.
|
|
87
|
+
* Uses regex triggers and heuristics.
|
|
88
|
+
* @param {string} message
|
|
89
|
+
* @returns {boolean}
|
|
90
|
+
*/
|
|
91
|
+
function looksLikeFact(message) {
|
|
92
|
+
if (!message || typeof message !== 'string') return false;
|
|
93
|
+
const trimmed = message.trim();
|
|
94
|
+
if (isNoise(trimmed)) return false;
|
|
95
|
+
if (isQuestion(trimmed)) return false;
|
|
96
|
+
if (isForgetRequest(trimmed)) return false;
|
|
97
|
+
// Check trigger patterns
|
|
98
|
+
for (const pattern of REMEMBER_TRIGGERS) {
|
|
99
|
+
if (pattern.test(trimmed)) return true;
|
|
100
|
+
}
|
|
101
|
+
// Heuristic: first-person statement longer than 5 words
|
|
102
|
+
if (/^(?:ich|i|mein|my)\s+\S+\s+\S+/i.test(trimmed) && trimmed.split(/\s+/).length > 4) {
|
|
103
|
+
return true;
|
|
104
|
+
}
|
|
105
|
+
// Statements about preferences, facts, experiences
|
|
106
|
+
if (/\b(?:präferenz|prefer|preference|favorite|liebling|erfahrung|experience|intergrund|background)\b/i.test(trimmed)) {
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Detect the language of a message (simple heuristic).
|
|
114
|
+
* @param {string} message
|
|
115
|
+
* @returns {'DE'|'EN'} Language code
|
|
116
|
+
*/
|
|
117
|
+
function detectLanguage(message) {
|
|
118
|
+
if (!message || typeof message !== 'string') return 'EN';
|
|
119
|
+
const germanChars = /[äöüßÄÖÜ]/g;
|
|
120
|
+
const deMatches = message.match(germanChars);
|
|
121
|
+
if (deMatches && deMatches.length > 0) return 'DE';
|
|
122
|
+
// Check common German words
|
|
123
|
+
const germanWords = /\b(ich|du|der|die|das|und|ist|nicht|ein|eine|zu|mit|auf|für|von|werde|habe|hat|sein|aus|bei|nach|mit|auch|es|an|als|noch|so|dann|wenn|nur|kann|sein)\b/gi;
|
|
124
|
+
const deCount = (message.match(germanWords) || []).length;
|
|
125
|
+
if (deCount > 2) return 'DE';
|
|
126
|
+
return 'EN';
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Determine if a user message should trigger the learning loop.
|
|
131
|
+
* @param {string} userMessage - The raw user message
|
|
132
|
+
* @returns {{should: boolean, reason: string, fact: string|null}}
|
|
133
|
+
*/
|
|
134
|
+
export function shouldRemember(userMessage) {
|
|
135
|
+
if (!userMessage || typeof userMessage !== 'string') {
|
|
136
|
+
return { should: false, reason: 'empty', fact: null };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Check for explicit trigger commands first (MERKE, LEARN etc.)
|
|
140
|
+
const parsed = triggerParser.parse(userMessage);
|
|
141
|
+
if (parsed && parsed.trigger && parsed.trigger.id === 'MERKE') {
|
|
142
|
+
// Explicit learn command — always learn the payload
|
|
143
|
+
return {
|
|
144
|
+
should: true,
|
|
145
|
+
reason: 'explicit-trigger',
|
|
146
|
+
fact: parsed.payload,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Check for forget request
|
|
151
|
+
if (isForgetRequest(userMessage)) {
|
|
152
|
+
return { should: false, reason: 'forget-request', fact: null };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Check learn mode
|
|
156
|
+
const mode = learnMode.getMode();
|
|
157
|
+
|
|
158
|
+
// In explicit mode, only explicit triggers are allowed
|
|
159
|
+
if (mode === 'explicit') {
|
|
160
|
+
return { should: false, reason: 'explicit-mode', fact: null };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// In safe mode, detect facts and ask
|
|
164
|
+
if (mode === 'safe') {
|
|
165
|
+
if (looksLikeFact(userMessage)) {
|
|
166
|
+
return {
|
|
167
|
+
should: true,
|
|
168
|
+
reason: 'safe-detect',
|
|
169
|
+
fact: userMessage.trim(),
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
return { should: false, reason: 'not-a-fact', fact: null };
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// In auto mode, detect facts and auto-store with confirmation
|
|
176
|
+
if (mode === 'auto') {
|
|
177
|
+
if (looksLikeFact(userMessage)) {
|
|
178
|
+
return {
|
|
179
|
+
should: true,
|
|
180
|
+
reason: 'auto-detect',
|
|
181
|
+
fact: userMessage.trim(),
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
return { should: false, reason: 'not-a-fact', fact: null };
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return { should: false, reason: 'unknown-mode', fact: null };
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Generate the confirmation prompt in the user's language.
|
|
192
|
+
* @param {string} fact - The fact to confirm
|
|
193
|
+
* @param {string} [lang] - Force language ('DE' | 'EN')
|
|
194
|
+
* @returns {string} The confirmation question
|
|
195
|
+
*/
|
|
196
|
+
export function promptConfirmation(fact, lang) {
|
|
197
|
+
const language = lang || detectLanguage(fact);
|
|
198
|
+
|
|
199
|
+
if (language === 'DE') {
|
|
200
|
+
return `Soll ich mir das merken? "${fact}"`;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return `Should I remember this? "${fact}"`;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Store a confirmed fact to encrypted memory.
|
|
208
|
+
* @param {string} userId - User identifier
|
|
209
|
+
* @param {string} fact - The confirmed fact
|
|
210
|
+
* @returns {object} Updated memory
|
|
211
|
+
*/
|
|
212
|
+
export function storeConfirmed(userId, fact) {
|
|
213
|
+
if (!fact || typeof fact !== 'string') {
|
|
214
|
+
throw new Error('Fact is required for storage');
|
|
215
|
+
}
|
|
216
|
+
return memoryStore.addFact(fact.trim(), false);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Mark a fact as rejected/ignored (never ask about it again).
|
|
221
|
+
* @param {string} userId - User identifier
|
|
222
|
+
* @param {string} fact - The rejected fact
|
|
223
|
+
* @returns {object} Updated memory (rejected list)
|
|
224
|
+
*/
|
|
225
|
+
export function rejectLearning(userId, fact) {
|
|
226
|
+
if (!fact || typeof fact !== 'string') {
|
|
227
|
+
throw new Error('Fact is required');
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Store in a special rejected facts list within memory
|
|
231
|
+
const memory = memoryStore.readMemory();
|
|
232
|
+
if (!memory.rejectedFacts) {
|
|
233
|
+
memory.rejectedFacts = [];
|
|
234
|
+
}
|
|
235
|
+
const trimmed = fact.trim();
|
|
236
|
+
if (!memory.rejectedFacts.includes(trimmed)) {
|
|
237
|
+
memory.rejectedFacts.push(trimmed);
|
|
238
|
+
}
|
|
239
|
+
memoryStore.writeMemory(memory);
|
|
240
|
+
return memory;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Check if a fact was previously rejected.
|
|
245
|
+
* @param {string} fact
|
|
246
|
+
* @returns {boolean}
|
|
247
|
+
*/
|
|
248
|
+
export function wasRejected(fact) {
|
|
249
|
+
if (!fact) return false;
|
|
250
|
+
const memory = memoryStore.readMemory();
|
|
251
|
+
return (memory.rejectedFacts || []).includes(fact.trim());
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
export default { shouldRemember, promptConfirmation, storeConfirmed, rejectLearning, wasRejected };
|