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.
@@ -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 };