ideaco 1.1.5
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/.dockerignore +33 -0
- package/.nvmrc +1 -0
- package/ARCHITECTURE.md +394 -0
- package/Dockerfile +50 -0
- package/LICENSE +29 -0
- package/README.md +206 -0
- package/bin/i18n.js +46 -0
- package/bin/ideaco.js +494 -0
- package/deploy.sh +15 -0
- package/docker-compose.yml +30 -0
- package/electron/main.cjs +986 -0
- package/electron/preload.cjs +14 -0
- package/electron/web-backends.cjs +854 -0
- package/jsconfig.json +8 -0
- package/next.config.mjs +34 -0
- package/package.json +134 -0
- package/postcss.config.mjs +6 -0
- package/public/demo/dashboard.png +0 -0
- package/public/demo/employee.png +0 -0
- package/public/demo/messages.png +0 -0
- package/public/demo/office.png +0 -0
- package/public/demo/requirement.png +0 -0
- package/public/logo.jpeg +0 -0
- package/public/logo.png +0 -0
- package/scripts/prepare-electron.js +67 -0
- package/scripts/release.js +76 -0
- package/src/app/api/agents/[agentId]/chat/route.js +70 -0
- package/src/app/api/agents/[agentId]/conversations/route.js +35 -0
- package/src/app/api/agents/[agentId]/route.js +106 -0
- package/src/app/api/avatar/route.js +104 -0
- package/src/app/api/browse-dir/route.js +44 -0
- package/src/app/api/chat/route.js +265 -0
- package/src/app/api/company/factory-reset/route.js +43 -0
- package/src/app/api/company/route.js +82 -0
- package/src/app/api/departments/[deptId]/agents/[agentId]/dismiss/route.js +19 -0
- package/src/app/api/departments/route.js +92 -0
- package/src/app/api/group-chat-loop/events/route.js +70 -0
- package/src/app/api/group-chat-loop/route.js +94 -0
- package/src/app/api/mailbox/route.js +100 -0
- package/src/app/api/messages/route.js +14 -0
- package/src/app/api/providers/[id]/configure/route.js +21 -0
- package/src/app/api/providers/[id]/refresh-cookie/route.js +38 -0
- package/src/app/api/providers/[id]/test-cookie/route.js +28 -0
- package/src/app/api/providers/route.js +11 -0
- package/src/app/api/requirements/route.js +242 -0
- package/src/app/api/secretary/route.js +65 -0
- package/src/app/api/system/cli-backends/route.js +91 -0
- package/src/app/api/system/cron/route.js +110 -0
- package/src/app/api/system/knowledge/route.js +104 -0
- package/src/app/api/system/plugins/route.js +40 -0
- package/src/app/api/system/skills/route.js +46 -0
- package/src/app/api/system/status/route.js +46 -0
- package/src/app/api/talent-market/[profileId]/recall/route.js +22 -0
- package/src/app/api/talent-market/[profileId]/route.js +17 -0
- package/src/app/api/talent-market/route.js +26 -0
- package/src/app/api/teams/route.js +773 -0
- package/src/app/api/ws-files/[departmentId]/file/route.js +27 -0
- package/src/app/api/ws-files/[departmentId]/files/route.js +22 -0
- package/src/app/globals.css +130 -0
- package/src/app/layout.jsx +40 -0
- package/src/app/page.jsx +97 -0
- package/src/components/AgentChatModal.jsx +164 -0
- package/src/components/AgentDetailModal.jsx +425 -0
- package/src/components/AgentSpyModal.jsx +481 -0
- package/src/components/AvatarGrid.jsx +29 -0
- package/src/components/BossProfileModal.jsx +162 -0
- package/src/components/CachedAvatar.jsx +77 -0
- package/src/components/ChatPanel.jsx +219 -0
- package/src/components/ChatShared.jsx +255 -0
- package/src/components/DepartmentDetail.jsx +842 -0
- package/src/components/DepartmentView.jsx +367 -0
- package/src/components/FileReference.jsx +260 -0
- package/src/components/FilesView.jsx +465 -0
- package/src/components/GroupChatView.jsx +799 -0
- package/src/components/Mailbox.jsx +926 -0
- package/src/components/MessagesView.jsx +112 -0
- package/src/components/OnboardingGuide.jsx +209 -0
- package/src/components/OrgTree.jsx +151 -0
- package/src/components/Overview.jsx +391 -0
- package/src/components/PixelOffice.jsx +2281 -0
- package/src/components/ProviderGrid.jsx +551 -0
- package/src/components/ProvidersBoard.jsx +16 -0
- package/src/components/RequirementDetail.jsx +1279 -0
- package/src/components/RequirementsBoard.jsx +187 -0
- package/src/components/SecretarySettings.jsx +295 -0
- package/src/components/SetupWizard.jsx +388 -0
- package/src/components/Sidebar.jsx +169 -0
- package/src/components/SystemMonitor.jsx +808 -0
- package/src/components/TalentMarket.jsx +183 -0
- package/src/components/TeamDetail.jsx +697 -0
- package/src/core/agent/base-agent.js +104 -0
- package/src/core/agent/chat-store.js +602 -0
- package/src/core/agent/cli-agent/backends/claude-code/README.md +52 -0
- package/src/core/agent/cli-agent/backends/claude-code/config.js +27 -0
- package/src/core/agent/cli-agent/backends/codebuddy/README.md +236 -0
- package/src/core/agent/cli-agent/backends/codebuddy/config.js +27 -0
- package/src/core/agent/cli-agent/backends/codex/README.md +51 -0
- package/src/core/agent/cli-agent/backends/codex/config.js +27 -0
- package/src/core/agent/cli-agent/backends/index.js +27 -0
- package/src/core/agent/cli-agent/backends/registry.js +580 -0
- package/src/core/agent/cli-agent/index.js +154 -0
- package/src/core/agent/index.js +60 -0
- package/src/core/agent/llm-agent/client.js +320 -0
- package/src/core/agent/llm-agent/index.js +97 -0
- package/src/core/agent/message-bus.js +211 -0
- package/src/core/agent/session.js +608 -0
- package/src/core/agent/tools.js +596 -0
- package/src/core/agent/web-agent/backends/base-backend.js +180 -0
- package/src/core/agent/web-agent/backends/chatgpt/client.js +146 -0
- package/src/core/agent/web-agent/backends/chatgpt/config.js +148 -0
- package/src/core/agent/web-agent/backends/chatgpt/dom-scripts.js +303 -0
- package/src/core/agent/web-agent/backends/index.js +91 -0
- package/src/core/agent/web-agent/index.js +278 -0
- package/src/core/agent/web-agent/web-client.js +407 -0
- package/src/core/employee/base-employee.js +1088 -0
- package/src/core/employee/index.js +35 -0
- package/src/core/employee/knowledge.js +327 -0
- package/src/core/employee/lifecycle.js +990 -0
- package/src/core/employee/memory/index.js +642 -0
- package/src/core/employee/memory/store.js +143 -0
- package/src/core/employee/performance.js +224 -0
- package/src/core/employee/secretary.js +625 -0
- package/src/core/employee/skills.js +398 -0
- package/src/core/index.js +38 -0
- package/src/core/organization/company.js +2600 -0
- package/src/core/organization/department.js +737 -0
- package/src/core/organization/group-chat-loop.js +264 -0
- package/src/core/organization/index.js +8 -0
- package/src/core/organization/persistence.js +111 -0
- package/src/core/organization/team.js +267 -0
- package/src/core/organization/workforce/hr.js +377 -0
- package/src/core/organization/workforce/providers.js +468 -0
- package/src/core/organization/workforce/role-archetypes.js +805 -0
- package/src/core/organization/workforce/talent-market.js +205 -0
- package/src/core/prompts.js +532 -0
- package/src/core/requirement.js +1789 -0
- package/src/core/system/audit.js +483 -0
- package/src/core/system/cron.js +449 -0
- package/src/core/system/index.js +7 -0
- package/src/core/system/plugin.js +2183 -0
- package/src/core/utils/json-parse.js +188 -0
- package/src/core/workspace.js +239 -0
- package/src/lib/api-i18n.js +211 -0
- package/src/lib/avatar.js +268 -0
- package/src/lib/client-store.js +1025 -0
- package/src/lib/config-validator.js +483 -0
- package/src/lib/format-time.js +22 -0
- package/src/lib/hooks.js +414 -0
- package/src/lib/i18n.js +134 -0
- package/src/lib/paths.js +23 -0
- package/src/lib/store.js +72 -0
- package/src/locales/de.js +393 -0
- package/src/locales/en.js +1054 -0
- package/src/locales/es.js +393 -0
- package/src/locales/fr.js +393 -0
- package/src/locales/ja.js +501 -0
- package/src/locales/ko.js +513 -0
- package/src/locales/zh.js +828 -0
- package/tailwind.config.mjs +11 -0
|
@@ -0,0 +1,642 @@
|
|
|
1
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Memory System - Each Employee has independent long-term and short-term memory
|
|
5
|
+
*
|
|
6
|
+
* Short-term memory: temporary info related to current task, may be forgotten or consolidated after task ends
|
|
7
|
+
* - Has expiration time (default 24 hours), expired entries are auto-cleaned
|
|
8
|
+
* - Unnecessary short-term memories are cleaned during consolidation
|
|
9
|
+
* Long-term memory: lessons learned, skill growth, self-reflection and other persistent info, stays with Employee permanently
|
|
10
|
+
*
|
|
11
|
+
* Rolling Context System:
|
|
12
|
+
* - historySummary: per-group rolling summary of old messages (compressed by AI)
|
|
13
|
+
* - AI manages its own memory via memoryOps in chat responses
|
|
14
|
+
*
|
|
15
|
+
* Relationship Impressions:
|
|
16
|
+
* - relationships: Map<employeeId, { name, impression, affinity, updatedAt }>
|
|
17
|
+
* - Each employee maintains a personal impression of every colleague they've interacted with
|
|
18
|
+
* - Impressions are โค200 chars, affinity is 1-100 (50=neutral), updated by AI via relationshipOps
|
|
19
|
+
*/
|
|
20
|
+
export class Memory {
|
|
21
|
+
constructor() {
|
|
22
|
+
this.shortTerm = []; // Short-term memory list
|
|
23
|
+
this.longTerm = []; // Long-term memory list
|
|
24
|
+
this.maxShortTerm = 20; // Max short-term capacity, oldest auto-evicted when exceeded
|
|
25
|
+
this.maxLongTerm = 50; // Max long-term capacity for active use (pruned by importance)
|
|
26
|
+
this.defaultShortTermTTL = 24 * 60 * 60 * 1000; // Default short-term memory TTL: 24 hours
|
|
27
|
+
|
|
28
|
+
// Per-group rolling history summary (compressed old messages)
|
|
29
|
+
// key: groupId, value: string (compressed summary text)
|
|
30
|
+
this.historySummary = new Map();
|
|
31
|
+
this.maxSummaryLength = 2000; // Max chars for a single group's summary
|
|
32
|
+
|
|
33
|
+
// Relationship impressions: how this employee perceives each colleague
|
|
34
|
+
// key: employeeId, value: { name: string, impression: string (โค200 chars), affinity: number (1-100), updatedAt: Date }
|
|
35
|
+
this.relationships = new Map();
|
|
36
|
+
this.maxImpressionLength = 200; // Max chars per impression
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// ======================== Short-term Memory ========================
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Add short-term memory
|
|
43
|
+
* @param {string} content - Memory content
|
|
44
|
+
* @param {string} [category] - Category tag
|
|
45
|
+
* @param {object} [options] - Additional options
|
|
46
|
+
* @param {number} [options.ttl] - Custom TTL in milliseconds
|
|
47
|
+
* @param {number} [options.importance] - Importance score 1-10 (default 5)
|
|
48
|
+
*/
|
|
49
|
+
addShortTerm(content, category = 'task', options = {}) {
|
|
50
|
+
const ttl = options.ttl || this.defaultShortTermTTL;
|
|
51
|
+
const memory = {
|
|
52
|
+
id: uuidv4(),
|
|
53
|
+
content,
|
|
54
|
+
category,
|
|
55
|
+
type: 'short-term',
|
|
56
|
+
importance: options.importance || 5,
|
|
57
|
+
createdAt: new Date(),
|
|
58
|
+
expiresAt: new Date(Date.now() + ttl),
|
|
59
|
+
};
|
|
60
|
+
this.shortTerm.push(memory);
|
|
61
|
+
|
|
62
|
+
// Evict oldest when capacity exceeded
|
|
63
|
+
if (this.shortTerm.length > this.maxShortTerm) {
|
|
64
|
+
const evicted = this.shortTerm.shift();
|
|
65
|
+
// Auto-archive evicted short-term memory as long-term summary
|
|
66
|
+
this.addLongTerm(
|
|
67
|
+
`[Auto-archived] ${evicted.content}`,
|
|
68
|
+
'archived',
|
|
69
|
+
{ importance: Math.max(1, (evicted.importance || 5) - 2) }
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return memory;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ======================== Long-term Memory ========================
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Add long-term memory
|
|
80
|
+
* @param {string} content - Memory content
|
|
81
|
+
* @param {string} [category] - Category: experience | reflection | skill | feedback | archived | preference | fact | instruction
|
|
82
|
+
* @param {object} [options]
|
|
83
|
+
* @param {number} [options.importance] - Importance score 1-10 (default 5)
|
|
84
|
+
*/
|
|
85
|
+
addLongTerm(content, category = 'experience', options = {}) {
|
|
86
|
+
const memory = {
|
|
87
|
+
id: uuidv4(),
|
|
88
|
+
content,
|
|
89
|
+
category,
|
|
90
|
+
type: 'long-term',
|
|
91
|
+
importance: options.importance || 5,
|
|
92
|
+
createdAt: new Date(),
|
|
93
|
+
};
|
|
94
|
+
this.longTerm.push(memory);
|
|
95
|
+
|
|
96
|
+
// Auto-prune if exceeds hard limit
|
|
97
|
+
if (this.longTerm.length > 200) {
|
|
98
|
+
this._pruneByImportance();
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return memory;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// ======================== Memory Operations (AI-driven) ========================
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Process memory operations returned by AI in chat response.
|
|
108
|
+
* This is the core of the AI-driven memory management system.
|
|
109
|
+
*
|
|
110
|
+
* @param {Array} memoryOps - Array of memory operations from AI
|
|
111
|
+
* Each op: { op: 'add'|'update'|'delete', type: 'long_term'|'short_term',
|
|
112
|
+
* content, importance, ttl, id, category }
|
|
113
|
+
* @returns {{ added: number, updated: number, deleted: number }}
|
|
114
|
+
*/
|
|
115
|
+
processMemoryOps(memoryOps) {
|
|
116
|
+
if (!Array.isArray(memoryOps) || memoryOps.length === 0) {
|
|
117
|
+
return { added: 0, updated: 0, deleted: 0 };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
let added = 0, updated = 0, deleted = 0;
|
|
121
|
+
|
|
122
|
+
for (const op of memoryOps) {
|
|
123
|
+
try {
|
|
124
|
+
switch (op.op) {
|
|
125
|
+
case 'add': {
|
|
126
|
+
if (!op.content) continue;
|
|
127
|
+
// Dedupe: skip if very similar content already exists
|
|
128
|
+
if (this._isDuplicate(op.content)) continue;
|
|
129
|
+
|
|
130
|
+
if (op.type === 'long_term') {
|
|
131
|
+
this.addLongTerm(op.content, op.category || 'experience', {
|
|
132
|
+
importance: op.importance || 5,
|
|
133
|
+
});
|
|
134
|
+
} else {
|
|
135
|
+
this.addShortTerm(op.content, op.category || 'context', {
|
|
136
|
+
importance: op.importance || 5,
|
|
137
|
+
ttl: op.ttl ? op.ttl * 1000 : undefined, // AI sends ttl in seconds
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
added++;
|
|
141
|
+
break;
|
|
142
|
+
}
|
|
143
|
+
case 'update': {
|
|
144
|
+
if (!op.id || !op.content) continue;
|
|
145
|
+
const mem = this._findById(op.id);
|
|
146
|
+
if (mem) {
|
|
147
|
+
mem.content = op.content;
|
|
148
|
+
if (op.importance !== undefined) mem.importance = op.importance;
|
|
149
|
+
updated++;
|
|
150
|
+
}
|
|
151
|
+
break;
|
|
152
|
+
}
|
|
153
|
+
case 'delete': {
|
|
154
|
+
if (!op.id) continue;
|
|
155
|
+
const stIdx = this.shortTerm.findIndex(m => m.id === op.id);
|
|
156
|
+
if (stIdx !== -1) {
|
|
157
|
+
this.shortTerm.splice(stIdx, 1);
|
|
158
|
+
deleted++;
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
const ltIdx = this.longTerm.findIndex(m => m.id === op.id);
|
|
162
|
+
if (ltIdx !== -1) {
|
|
163
|
+
this.longTerm.splice(ltIdx, 1);
|
|
164
|
+
deleted++;
|
|
165
|
+
}
|
|
166
|
+
break;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
} catch (e) {
|
|
170
|
+
console.warn(` โ ๏ธ [Memory] Failed to process memoryOp:`, op, e.message);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (added + updated + deleted > 0) {
|
|
175
|
+
console.log(` ๐ง [Memory] Processed ${added} adds, ${updated} updates, ${deleted} deletes`);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return { added, updated, deleted };
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// ======================== Rolling History Summary ========================
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Update the rolling history summary for a group.
|
|
185
|
+
* Called when AI returns a summary of old messages.
|
|
186
|
+
*
|
|
187
|
+
* @param {string} groupId
|
|
188
|
+
* @param {string} newSummary - AI-generated summary of old messages
|
|
189
|
+
*/
|
|
190
|
+
updateHistorySummary(groupId, newSummary) {
|
|
191
|
+
if (!newSummary || !newSummary.trim()) return;
|
|
192
|
+
|
|
193
|
+
const existing = this.historySummary.get(groupId) || '';
|
|
194
|
+
let combined;
|
|
195
|
+
|
|
196
|
+
if (existing) {
|
|
197
|
+
combined = `${existing}\n---\n${newSummary.trim()}`;
|
|
198
|
+
} else {
|
|
199
|
+
combined = newSummary.trim();
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Truncate if too long โ keep the most recent part
|
|
203
|
+
if (combined.length > this.maxSummaryLength) {
|
|
204
|
+
// Find a good split point (after a "---" separator)
|
|
205
|
+
const parts = combined.split('\n---\n');
|
|
206
|
+
while (parts.join('\n---\n').length > this.maxSummaryLength && parts.length > 1) {
|
|
207
|
+
parts.shift(); // Drop oldest summary chunk
|
|
208
|
+
}
|
|
209
|
+
combined = parts.join('\n---\n');
|
|
210
|
+
// If still too long, hard truncate
|
|
211
|
+
if (combined.length > this.maxSummaryLength) {
|
|
212
|
+
combined = combined.slice(-this.maxSummaryLength);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
this.historySummary.set(groupId, combined);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Get the rolling history summary for a group.
|
|
221
|
+
* @param {string} groupId
|
|
222
|
+
* @returns {string}
|
|
223
|
+
*/
|
|
224
|
+
getHistorySummary(groupId) {
|
|
225
|
+
return this.historySummary.get(groupId) || '';
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// ======================== Relationship Impressions ========================
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Process relationship operations returned by AI in chat response.
|
|
232
|
+
* @param {Array} relationshipOps - Array of relationship operations from AI
|
|
233
|
+
* Each op: { employeeId, name, impression, affinity }
|
|
234
|
+
* @returns {{ updated: number }}
|
|
235
|
+
*/
|
|
236
|
+
processRelationshipOps(relationshipOps) {
|
|
237
|
+
if (!Array.isArray(relationshipOps) || relationshipOps.length === 0) {
|
|
238
|
+
return { updated: 0 };
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
let updated = 0;
|
|
242
|
+
for (const op of relationshipOps) {
|
|
243
|
+
try {
|
|
244
|
+
if (!op.employeeId || !op.impression) continue;
|
|
245
|
+
const impression = op.impression.slice(0, this.maxImpressionLength);
|
|
246
|
+
const existing = this.relationships.get(op.employeeId);
|
|
247
|
+
// Clamp affinity to 1-100, default to existing or 50 (neutral)
|
|
248
|
+
let affinity = 50;
|
|
249
|
+
if (typeof op.affinity === 'number') {
|
|
250
|
+
affinity = Math.max(1, Math.min(100, Math.round(op.affinity)));
|
|
251
|
+
} else if (existing?.affinity) {
|
|
252
|
+
affinity = existing.affinity;
|
|
253
|
+
}
|
|
254
|
+
this.relationships.set(op.employeeId, {
|
|
255
|
+
name: op.name || existing?.name || op.employeeId,
|
|
256
|
+
impression,
|
|
257
|
+
affinity,
|
|
258
|
+
updatedAt: new Date(),
|
|
259
|
+
});
|
|
260
|
+
updated++;
|
|
261
|
+
} catch (e) {
|
|
262
|
+
console.warn(` โ ๏ธ [Memory] Failed to process relationshipOp:`, op, e.message);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
if (updated > 0) {
|
|
267
|
+
console.log(` ๐ฅ [Memory] Updated ${updated} relationship impressions`);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return { updated };
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Build a relationship context string for prompt injection.
|
|
275
|
+
* Only includes impressions for employees present in the given participant list.
|
|
276
|
+
* @param {Array<string>} participantIds - IDs of employees in the current conversation
|
|
277
|
+
* @returns {string}
|
|
278
|
+
*/
|
|
279
|
+
buildRelationshipContext(participantIds) {
|
|
280
|
+
if (!participantIds || participantIds.length === 0) return '';
|
|
281
|
+
|
|
282
|
+
const impressions = [];
|
|
283
|
+
for (const pid of participantIds) {
|
|
284
|
+
const rel = this.relationships.get(pid);
|
|
285
|
+
if (rel) {
|
|
286
|
+
const heart = rel.affinity >= 80 ? 'โค๏ธ' : rel.affinity >= 60 ? '๐' : rel.affinity >= 40 ? '๐' : rel.affinity >= 20 ? '๐' : '๐ข';
|
|
287
|
+
impressions.push(`- ${rel.name} (${pid}): ${rel.impression} ${heart} affinity:${rel.affinity}/100`);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
if (impressions.length === 0) return '';
|
|
292
|
+
return `\n\n**๐ฅ Your impressions of colleagues in this conversation:**\n${impressions.join('\n')}`;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Get impression of a specific employee.
|
|
297
|
+
* @param {string} employeeId
|
|
298
|
+
* @returns {{ name: string, impression: string, affinity: number, updatedAt: Date } | null}
|
|
299
|
+
*/
|
|
300
|
+
getImpression(employeeId) {
|
|
301
|
+
return this.relationships.get(employeeId) || null;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Get all relationship impressions.
|
|
306
|
+
* @returns {Array<{ employeeId: string, name: string, impression: string, affinity: number, updatedAt: Date }>}
|
|
307
|
+
*/
|
|
308
|
+
getAllImpressions() {
|
|
309
|
+
const result = [];
|
|
310
|
+
for (const [id, rel] of this.relationships) {
|
|
311
|
+
result.push({ employeeId: id, ...rel });
|
|
312
|
+
}
|
|
313
|
+
return result;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// ======================== Context Builder ========================
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Build a compact memory context string for inclusion in prompts.
|
|
320
|
+
* This replaces the old agentMemory approach with structured memory.
|
|
321
|
+
*
|
|
322
|
+
* @param {string} groupId - Current group context
|
|
323
|
+
* @returns {string} Formatted memory context for prompt injection
|
|
324
|
+
*/
|
|
325
|
+
buildMemoryContext(groupId) {
|
|
326
|
+
const parts = [];
|
|
327
|
+
|
|
328
|
+
// 1. Rolling history summary
|
|
329
|
+
const summary = this.getHistorySummary(groupId);
|
|
330
|
+
if (summary) {
|
|
331
|
+
parts.push(`**๐ Conversation History Summary:**\n${summary}`);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// 2. Long-term memories (sorted by importance, top items)
|
|
335
|
+
const activeLongTerm = this.longTerm
|
|
336
|
+
.sort((a, b) => (b.importance || 5) - (a.importance || 5))
|
|
337
|
+
.slice(0, 15);
|
|
338
|
+
if (activeLongTerm.length > 0) {
|
|
339
|
+
parts.push(`**๐พ Your Long-term Memories:**\n${activeLongTerm.map(m =>
|
|
340
|
+
`- [${m.category}] ${m.content} (id:${m.id})`
|
|
341
|
+
).join('\n')}`);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// 3. Short-term memories (filtered for active, not expired)
|
|
345
|
+
this.cleanExpiredShortTerm();
|
|
346
|
+
const activeShortTerm = this.shortTerm
|
|
347
|
+
.sort((a, b) => (b.importance || 5) - (a.importance || 5))
|
|
348
|
+
.slice(0, 10);
|
|
349
|
+
if (activeShortTerm.length > 0) {
|
|
350
|
+
parts.push(`**โก Your Short-term Memories:**\n${activeShortTerm.map(m =>
|
|
351
|
+
`- [${m.category}] ${m.content} (id:${m.id})`
|
|
352
|
+
).join('\n')}`);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
return parts.length > 0 ? '\n\n' + parts.join('\n\n') : '';
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Build full context including memory + relationships for a group conversation.
|
|
360
|
+
* @param {string} groupId - Current group context
|
|
361
|
+
* @param {Array<string>} [participantIds] - IDs of employees in the conversation
|
|
362
|
+
* @returns {string}
|
|
363
|
+
*/
|
|
364
|
+
buildFullContext(groupId, participantIds = []) {
|
|
365
|
+
let ctx = this.buildMemoryContext(groupId);
|
|
366
|
+
const relCtx = this.buildRelationshipContext(participantIds);
|
|
367
|
+
if (relCtx) ctx += relCtx;
|
|
368
|
+
return ctx;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// ======================== Existing Methods (preserved) ========================
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Consolidate short-term memory into long-term
|
|
375
|
+
* @param {string} shortTermId - Short-term memory ID
|
|
376
|
+
* @param {string} [refinedContent] - Refined content (uses original if not provided)
|
|
377
|
+
*/
|
|
378
|
+
consolidate(shortTermId, refinedContent = null) {
|
|
379
|
+
const idx = this.shortTerm.findIndex(m => m.id === shortTermId);
|
|
380
|
+
if (idx === -1) return null;
|
|
381
|
+
|
|
382
|
+
const original = this.shortTerm.splice(idx, 1)[0];
|
|
383
|
+
const longMemory = this.addLongTerm(
|
|
384
|
+
refinedContent || original.content,
|
|
385
|
+
'experience'
|
|
386
|
+
);
|
|
387
|
+
|
|
388
|
+
return longMemory;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* Clean up expired short-term memories
|
|
393
|
+
* @returns {number} Number of cleaned memories
|
|
394
|
+
*/
|
|
395
|
+
cleanExpiredShortTerm() {
|
|
396
|
+
const now = Date.now();
|
|
397
|
+
const before = this.shortTerm.length;
|
|
398
|
+
this.shortTerm = this.shortTerm.filter(m => {
|
|
399
|
+
if (!m.expiresAt) return true;
|
|
400
|
+
return new Date(m.expiresAt).getTime() > now;
|
|
401
|
+
});
|
|
402
|
+
const cleaned = before - this.shortTerm.length;
|
|
403
|
+
if (cleaned > 0) {
|
|
404
|
+
console.log(`๐งน Cleaned ${cleaned} expired short-term memories`);
|
|
405
|
+
}
|
|
406
|
+
return cleaned;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* Consolidate memories: clean expired short-term, deduplicate, and limit total long-term count
|
|
411
|
+
* Should be called before waking up an Employee
|
|
412
|
+
* @returns {object} { expiredCleaned, duplicatesRemoved }
|
|
413
|
+
*/
|
|
414
|
+
consolidateMemories() {
|
|
415
|
+
// 1. Clean expired short-term memories
|
|
416
|
+
const expiredCleaned = this.cleanExpiredShortTerm();
|
|
417
|
+
|
|
418
|
+
// 2. Deduplicate long-term memories
|
|
419
|
+
const seen = new Map();
|
|
420
|
+
const deduped = [];
|
|
421
|
+
let duplicatesRemoved = 0;
|
|
422
|
+
|
|
423
|
+
for (const mem of this.longTerm) {
|
|
424
|
+
const key = mem.content.toLowerCase().trim().slice(0, 100);
|
|
425
|
+
if (seen.has(key)) {
|
|
426
|
+
duplicatesRemoved++;
|
|
427
|
+
continue;
|
|
428
|
+
}
|
|
429
|
+
seen.set(key, true);
|
|
430
|
+
deduped.push(mem);
|
|
431
|
+
}
|
|
432
|
+
this.longTerm = deduped;
|
|
433
|
+
|
|
434
|
+
// 3. If long-term memory exceeds limit, prune by importance
|
|
435
|
+
if (this.longTerm.length > 200) {
|
|
436
|
+
this._pruneByImportance();
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
if (expiredCleaned > 0 || duplicatesRemoved > 0) {
|
|
440
|
+
console.log(`๐ง Memory consolidation complete: cleaned ${expiredCleaned} expired short-term, deduplicated ${duplicatesRemoved} long-term`);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
return { expiredCleaned, duplicatesRemoved };
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* Clear short-term memory (e.g. when switching department/project)
|
|
448
|
+
*/
|
|
449
|
+
clearShortTerm() {
|
|
450
|
+
this.shortTerm = [];
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* Search long-term memory by category
|
|
455
|
+
*/
|
|
456
|
+
searchLongTerm(category = null) {
|
|
457
|
+
if (!category) return [...this.longTerm];
|
|
458
|
+
return this.longTerm.filter(m => m.category === category);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/**
|
|
462
|
+
* Search all memory by keyword
|
|
463
|
+
*/
|
|
464
|
+
search(keyword) {
|
|
465
|
+
const kw = keyword.toLowerCase();
|
|
466
|
+
const results = [];
|
|
467
|
+
[...this.shortTerm, ...this.longTerm].forEach(m => {
|
|
468
|
+
if (m.content.toLowerCase().includes(kw)) {
|
|
469
|
+
results.push(m);
|
|
470
|
+
}
|
|
471
|
+
});
|
|
472
|
+
return results;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/**
|
|
476
|
+
* Get memory summary
|
|
477
|
+
*/
|
|
478
|
+
getSummary() {
|
|
479
|
+
return {
|
|
480
|
+
shortTermCount: this.shortTerm.length,
|
|
481
|
+
longTermCount: this.longTerm.length,
|
|
482
|
+
historySummaryGroups: this.historySummary.size,
|
|
483
|
+
relationshipCount: this.relationships.size,
|
|
484
|
+
shortTerm: this.shortTerm.map(m => ({
|
|
485
|
+
id: m.id,
|
|
486
|
+
content: m.content,
|
|
487
|
+
category: m.category,
|
|
488
|
+
importance: m.importance || 5,
|
|
489
|
+
expiresAt: m.expiresAt,
|
|
490
|
+
})),
|
|
491
|
+
longTerm: this.longTerm.map(m => ({
|
|
492
|
+
id: m.id,
|
|
493
|
+
content: m.content,
|
|
494
|
+
category: m.category,
|
|
495
|
+
importance: m.importance || 5,
|
|
496
|
+
})),
|
|
497
|
+
relationships: this.getAllImpressions().map(r => ({
|
|
498
|
+
employeeId: r.employeeId,
|
|
499
|
+
name: r.name,
|
|
500
|
+
impression: r.impression,
|
|
501
|
+
affinity: r.affinity || 50,
|
|
502
|
+
updatedAt: r.updatedAt,
|
|
503
|
+
})),
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// ======================== Serialization ========================
|
|
508
|
+
|
|
509
|
+
/**
|
|
510
|
+
* Serialize (for persistence / talent market storage)
|
|
511
|
+
*/
|
|
512
|
+
serialize() {
|
|
513
|
+
// Serialize historySummary Map โ plain object
|
|
514
|
+
const summaryObj = {};
|
|
515
|
+
for (const [k, v] of this.historySummary) {
|
|
516
|
+
summaryObj[k] = v;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// Serialize relationships Map โ plain object
|
|
520
|
+
const relObj = {};
|
|
521
|
+
for (const [k, v] of this.relationships) {
|
|
522
|
+
relObj[k] = {
|
|
523
|
+
...v,
|
|
524
|
+
updatedAt: v.updatedAt instanceof Date ? v.updatedAt.toISOString() : v.updatedAt,
|
|
525
|
+
};
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
return {
|
|
529
|
+
shortTerm: this.shortTerm.map(m => ({
|
|
530
|
+
...m,
|
|
531
|
+
createdAt: m.createdAt instanceof Date ? m.createdAt.toISOString() : m.createdAt,
|
|
532
|
+
expiresAt: m.expiresAt ? (m.expiresAt instanceof Date ? m.expiresAt.toISOString() : m.expiresAt) : null,
|
|
533
|
+
})),
|
|
534
|
+
longTerm: this.longTerm.map(m => ({
|
|
535
|
+
...m,
|
|
536
|
+
createdAt: m.createdAt instanceof Date ? m.createdAt.toISOString() : m.createdAt,
|
|
537
|
+
})),
|
|
538
|
+
historySummary: summaryObj,
|
|
539
|
+
relationships: relObj,
|
|
540
|
+
};
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
/**
|
|
544
|
+
* Restore from serialized data
|
|
545
|
+
*/
|
|
546
|
+
static deserialize(data) {
|
|
547
|
+
const memory = new Memory();
|
|
548
|
+
if (data.shortTerm) {
|
|
549
|
+
memory.shortTerm = data.shortTerm.map(m => ({
|
|
550
|
+
...m,
|
|
551
|
+
createdAt: m.createdAt ? new Date(m.createdAt) : new Date(),
|
|
552
|
+
expiresAt: m.expiresAt ? new Date(m.expiresAt) : new Date(Date.now() + memory.defaultShortTermTTL),
|
|
553
|
+
}));
|
|
554
|
+
}
|
|
555
|
+
if (data.longTerm) {
|
|
556
|
+
memory.longTerm = data.longTerm.map(m => ({
|
|
557
|
+
...m,
|
|
558
|
+
createdAt: m.createdAt ? new Date(m.createdAt) : new Date(),
|
|
559
|
+
}));
|
|
560
|
+
}
|
|
561
|
+
// Restore historySummary
|
|
562
|
+
if (data.historySummary && typeof data.historySummary === 'object') {
|
|
563
|
+
for (const [k, v] of Object.entries(data.historySummary)) {
|
|
564
|
+
memory.historySummary.set(k, v);
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
// Restore relationships
|
|
568
|
+
if (data.relationships && typeof data.relationships === 'object') {
|
|
569
|
+
for (const [k, v] of Object.entries(data.relationships)) {
|
|
570
|
+
memory.relationships.set(k, {
|
|
571
|
+
...v,
|
|
572
|
+
updatedAt: v.updatedAt ? new Date(v.updatedAt) : new Date(),
|
|
573
|
+
});
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
return memory;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
// ======================== Private Helpers ========================
|
|
580
|
+
|
|
581
|
+
/**
|
|
582
|
+
* Find a memory by ID across both short-term and long-term.
|
|
583
|
+
*/
|
|
584
|
+
_findById(id) {
|
|
585
|
+
return this.shortTerm.find(m => m.id === id) || this.longTerm.find(m => m.id === id) || null;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
/**
|
|
589
|
+
* Check if content is a duplicate of existing memory.
|
|
590
|
+
* Uses first 80 chars as fingerprint.
|
|
591
|
+
*/
|
|
592
|
+
_isDuplicate(content) {
|
|
593
|
+
const key = content.toLowerCase().trim().slice(0, 80);
|
|
594
|
+
return [...this.shortTerm, ...this.longTerm].some(m =>
|
|
595
|
+
m.content.toLowerCase().trim().slice(0, 80) === key
|
|
596
|
+
);
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
/**
|
|
600
|
+
* Prune long-term memory by importance when over capacity.
|
|
601
|
+
* Keeps the highest-importance and most-recent entries.
|
|
602
|
+
*/
|
|
603
|
+
_pruneByImportance() {
|
|
604
|
+
const MAX = 200;
|
|
605
|
+
if (this.longTerm.length <= MAX) return;
|
|
606
|
+
|
|
607
|
+
// Sort by importance (desc), then by creation time (desc)
|
|
608
|
+
this.longTerm.sort((a, b) => {
|
|
609
|
+
const impDiff = (b.importance || 5) - (a.importance || 5);
|
|
610
|
+
if (impDiff !== 0) return impDiff;
|
|
611
|
+
return new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime();
|
|
612
|
+
});
|
|
613
|
+
|
|
614
|
+
const trimmed = this.longTerm.length - MAX;
|
|
615
|
+
this.longTerm = this.longTerm.slice(0, MAX);
|
|
616
|
+
console.log(`๐งน Long-term memory pruned by importance: removed ${trimmed} lowest-importance entries`);
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
/**
|
|
620
|
+
* Print memory contents (debug)
|
|
621
|
+
*/
|
|
622
|
+
print(agentName = '') {
|
|
623
|
+
const prefix = agentName ? `[${agentName}]` : '';
|
|
624
|
+
console.log(`\n๐ง ${prefix} Memory System:`);
|
|
625
|
+
console.log(` Short-term (${this.shortTerm.length} items):`);
|
|
626
|
+
this.shortTerm.forEach(m => {
|
|
627
|
+
const expires = m.expiresAt ? ` [expires: ${new Date(m.expiresAt).toLocaleString()}]` : '';
|
|
628
|
+
const imp = m.importance ? ` [imp:${m.importance}]` : '';
|
|
629
|
+
console.log(` ๐ [${m.category}] ${m.content}${expires}${imp}`);
|
|
630
|
+
});
|
|
631
|
+
console.log(` Long-term (${this.longTerm.length} items):`);
|
|
632
|
+
this.longTerm.forEach(m => {
|
|
633
|
+
const imp = m.importance ? ` [imp:${m.importance}]` : '';
|
|
634
|
+
console.log(` ๐พ [${m.category}] ${m.content}${imp}`);
|
|
635
|
+
});
|
|
636
|
+
console.log(` History Summaries: ${this.historySummary.size} groups`);
|
|
637
|
+
console.log(` Relationships (${this.relationships.size} colleagues):`);
|
|
638
|
+
for (const [id, rel] of this.relationships) {
|
|
639
|
+
console.log(` ๐ค ${rel.name} (${id}): ${rel.impression}`);
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
}
|