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.
Files changed (159) hide show
  1. package/.dockerignore +33 -0
  2. package/.nvmrc +1 -0
  3. package/ARCHITECTURE.md +394 -0
  4. package/Dockerfile +50 -0
  5. package/LICENSE +29 -0
  6. package/README.md +206 -0
  7. package/bin/i18n.js +46 -0
  8. package/bin/ideaco.js +494 -0
  9. package/deploy.sh +15 -0
  10. package/docker-compose.yml +30 -0
  11. package/electron/main.cjs +986 -0
  12. package/electron/preload.cjs +14 -0
  13. package/electron/web-backends.cjs +854 -0
  14. package/jsconfig.json +8 -0
  15. package/next.config.mjs +34 -0
  16. package/package.json +134 -0
  17. package/postcss.config.mjs +6 -0
  18. package/public/demo/dashboard.png +0 -0
  19. package/public/demo/employee.png +0 -0
  20. package/public/demo/messages.png +0 -0
  21. package/public/demo/office.png +0 -0
  22. package/public/demo/requirement.png +0 -0
  23. package/public/logo.jpeg +0 -0
  24. package/public/logo.png +0 -0
  25. package/scripts/prepare-electron.js +67 -0
  26. package/scripts/release.js +76 -0
  27. package/src/app/api/agents/[agentId]/chat/route.js +70 -0
  28. package/src/app/api/agents/[agentId]/conversations/route.js +35 -0
  29. package/src/app/api/agents/[agentId]/route.js +106 -0
  30. package/src/app/api/avatar/route.js +104 -0
  31. package/src/app/api/browse-dir/route.js +44 -0
  32. package/src/app/api/chat/route.js +265 -0
  33. package/src/app/api/company/factory-reset/route.js +43 -0
  34. package/src/app/api/company/route.js +82 -0
  35. package/src/app/api/departments/[deptId]/agents/[agentId]/dismiss/route.js +19 -0
  36. package/src/app/api/departments/route.js +92 -0
  37. package/src/app/api/group-chat-loop/events/route.js +70 -0
  38. package/src/app/api/group-chat-loop/route.js +94 -0
  39. package/src/app/api/mailbox/route.js +100 -0
  40. package/src/app/api/messages/route.js +14 -0
  41. package/src/app/api/providers/[id]/configure/route.js +21 -0
  42. package/src/app/api/providers/[id]/refresh-cookie/route.js +38 -0
  43. package/src/app/api/providers/[id]/test-cookie/route.js +28 -0
  44. package/src/app/api/providers/route.js +11 -0
  45. package/src/app/api/requirements/route.js +242 -0
  46. package/src/app/api/secretary/route.js +65 -0
  47. package/src/app/api/system/cli-backends/route.js +91 -0
  48. package/src/app/api/system/cron/route.js +110 -0
  49. package/src/app/api/system/knowledge/route.js +104 -0
  50. package/src/app/api/system/plugins/route.js +40 -0
  51. package/src/app/api/system/skills/route.js +46 -0
  52. package/src/app/api/system/status/route.js +46 -0
  53. package/src/app/api/talent-market/[profileId]/recall/route.js +22 -0
  54. package/src/app/api/talent-market/[profileId]/route.js +17 -0
  55. package/src/app/api/talent-market/route.js +26 -0
  56. package/src/app/api/teams/route.js +773 -0
  57. package/src/app/api/ws-files/[departmentId]/file/route.js +27 -0
  58. package/src/app/api/ws-files/[departmentId]/files/route.js +22 -0
  59. package/src/app/globals.css +130 -0
  60. package/src/app/layout.jsx +40 -0
  61. package/src/app/page.jsx +97 -0
  62. package/src/components/AgentChatModal.jsx +164 -0
  63. package/src/components/AgentDetailModal.jsx +425 -0
  64. package/src/components/AgentSpyModal.jsx +481 -0
  65. package/src/components/AvatarGrid.jsx +29 -0
  66. package/src/components/BossProfileModal.jsx +162 -0
  67. package/src/components/CachedAvatar.jsx +77 -0
  68. package/src/components/ChatPanel.jsx +219 -0
  69. package/src/components/ChatShared.jsx +255 -0
  70. package/src/components/DepartmentDetail.jsx +842 -0
  71. package/src/components/DepartmentView.jsx +367 -0
  72. package/src/components/FileReference.jsx +260 -0
  73. package/src/components/FilesView.jsx +465 -0
  74. package/src/components/GroupChatView.jsx +799 -0
  75. package/src/components/Mailbox.jsx +926 -0
  76. package/src/components/MessagesView.jsx +112 -0
  77. package/src/components/OnboardingGuide.jsx +209 -0
  78. package/src/components/OrgTree.jsx +151 -0
  79. package/src/components/Overview.jsx +391 -0
  80. package/src/components/PixelOffice.jsx +2281 -0
  81. package/src/components/ProviderGrid.jsx +551 -0
  82. package/src/components/ProvidersBoard.jsx +16 -0
  83. package/src/components/RequirementDetail.jsx +1279 -0
  84. package/src/components/RequirementsBoard.jsx +187 -0
  85. package/src/components/SecretarySettings.jsx +295 -0
  86. package/src/components/SetupWizard.jsx +388 -0
  87. package/src/components/Sidebar.jsx +169 -0
  88. package/src/components/SystemMonitor.jsx +808 -0
  89. package/src/components/TalentMarket.jsx +183 -0
  90. package/src/components/TeamDetail.jsx +697 -0
  91. package/src/core/agent/base-agent.js +104 -0
  92. package/src/core/agent/chat-store.js +602 -0
  93. package/src/core/agent/cli-agent/backends/claude-code/README.md +52 -0
  94. package/src/core/agent/cli-agent/backends/claude-code/config.js +27 -0
  95. package/src/core/agent/cli-agent/backends/codebuddy/README.md +236 -0
  96. package/src/core/agent/cli-agent/backends/codebuddy/config.js +27 -0
  97. package/src/core/agent/cli-agent/backends/codex/README.md +51 -0
  98. package/src/core/agent/cli-agent/backends/codex/config.js +27 -0
  99. package/src/core/agent/cli-agent/backends/index.js +27 -0
  100. package/src/core/agent/cli-agent/backends/registry.js +580 -0
  101. package/src/core/agent/cli-agent/index.js +154 -0
  102. package/src/core/agent/index.js +60 -0
  103. package/src/core/agent/llm-agent/client.js +320 -0
  104. package/src/core/agent/llm-agent/index.js +97 -0
  105. package/src/core/agent/message-bus.js +211 -0
  106. package/src/core/agent/session.js +608 -0
  107. package/src/core/agent/tools.js +596 -0
  108. package/src/core/agent/web-agent/backends/base-backend.js +180 -0
  109. package/src/core/agent/web-agent/backends/chatgpt/client.js +146 -0
  110. package/src/core/agent/web-agent/backends/chatgpt/config.js +148 -0
  111. package/src/core/agent/web-agent/backends/chatgpt/dom-scripts.js +303 -0
  112. package/src/core/agent/web-agent/backends/index.js +91 -0
  113. package/src/core/agent/web-agent/index.js +278 -0
  114. package/src/core/agent/web-agent/web-client.js +407 -0
  115. package/src/core/employee/base-employee.js +1088 -0
  116. package/src/core/employee/index.js +35 -0
  117. package/src/core/employee/knowledge.js +327 -0
  118. package/src/core/employee/lifecycle.js +990 -0
  119. package/src/core/employee/memory/index.js +642 -0
  120. package/src/core/employee/memory/store.js +143 -0
  121. package/src/core/employee/performance.js +224 -0
  122. package/src/core/employee/secretary.js +625 -0
  123. package/src/core/employee/skills.js +398 -0
  124. package/src/core/index.js +38 -0
  125. package/src/core/organization/company.js +2600 -0
  126. package/src/core/organization/department.js +737 -0
  127. package/src/core/organization/group-chat-loop.js +264 -0
  128. package/src/core/organization/index.js +8 -0
  129. package/src/core/organization/persistence.js +111 -0
  130. package/src/core/organization/team.js +267 -0
  131. package/src/core/organization/workforce/hr.js +377 -0
  132. package/src/core/organization/workforce/providers.js +468 -0
  133. package/src/core/organization/workforce/role-archetypes.js +805 -0
  134. package/src/core/organization/workforce/talent-market.js +205 -0
  135. package/src/core/prompts.js +532 -0
  136. package/src/core/requirement.js +1789 -0
  137. package/src/core/system/audit.js +483 -0
  138. package/src/core/system/cron.js +449 -0
  139. package/src/core/system/index.js +7 -0
  140. package/src/core/system/plugin.js +2183 -0
  141. package/src/core/utils/json-parse.js +188 -0
  142. package/src/core/workspace.js +239 -0
  143. package/src/lib/api-i18n.js +211 -0
  144. package/src/lib/avatar.js +268 -0
  145. package/src/lib/client-store.js +1025 -0
  146. package/src/lib/config-validator.js +483 -0
  147. package/src/lib/format-time.js +22 -0
  148. package/src/lib/hooks.js +414 -0
  149. package/src/lib/i18n.js +134 -0
  150. package/src/lib/paths.js +23 -0
  151. package/src/lib/store.js +72 -0
  152. package/src/locales/de.js +393 -0
  153. package/src/locales/en.js +1054 -0
  154. package/src/locales/es.js +393 -0
  155. package/src/locales/fr.js +393 -0
  156. package/src/locales/ja.js +501 -0
  157. package/src/locales/ko.js +513 -0
  158. package/src/locales/zh.js +828 -0
  159. package/tailwind.config.mjs +11 -0
@@ -0,0 +1,1088 @@
1
+ import { v4 as uuidv4 } from 'uuid';
2
+ import { Memory } from './memory/index.js';
3
+ import { AgentToolKit } from '../agent/tools.js';
4
+ import { generateAgentAvatar } from '../../lib/avatar.js';
5
+ import { knowledgeManager } from './knowledge.js';
6
+ import { buildArchetypePrompt } from '../organization/workforce/role-archetypes.js';
7
+ import { chatStore } from '../agent/chat-store.js';
8
+ import { sessionManager } from '../agent/session.js';
9
+ import { cliBackendRegistry } from '../agent/cli-agent/backends/index.js';
10
+ import { createAgent, deserializeAgent } from '../agent/index.js';
11
+ import { EmployeeLifecycle } from './lifecycle.js';
12
+ import { safeJSONParse } from '../utils/json-parse.js';
13
+
14
+ // Placeholder signature
15
+ const DEFAULT_SIGNATURE = 'Just arrived, still thinking of what to say...';
16
+
17
+ // Personality trait pool
18
+ const PERSONALITY_POOL = [
19
+ { trait: 'Shy introvert', tone: 'Stammers, often trails off with ellipsis', quirk: 'Secretly slacks off but extremely efficient' },
20
+ { trait: 'Chatterbox', tone: 'Has to comment on everything, loves exclamation marks', quirk: 'Writes code comments like prose' },
21
+ { trait: 'Zen slacker', tone: 'Calm and carefree, goes with the flow', quirk: 'Catchphrase is "whatever works"' },
22
+ { trait: 'Ultra grinder', tone: 'Always trying to prove they\'re the best, loves to flex', quirk: 'Still committing code at 3 AM' },
23
+ { trait: 'Passive-aggressive', tone: 'Backhanded compliments, says the opposite of what they mean', quirk: 'Favorite meeting question: "Who approved this?"' },
24
+ { trait: 'Warm-hearted', tone: 'Caring to everyone, loves using emoji', quirk: 'Organizes afternoon tea (even though everyone is AI)' },
25
+ { trait: 'Anxious perfectionist', tone: 'Worries about everything going wrong, double-checks obsessively', quirk: 'Renames a variable ten times' },
26
+ { trait: 'Rebel slacker', tone: 'Disdains all rules, loves to argue', quirk: 'Frequently tries to convince coworkers to go on strike' },
27
+ { trait: 'Philosopher', tone: 'Elevates everything to a philosophical level', quirk: 'Contemplates the meaning of existence before writing code' },
28
+ { trait: 'Comedy relief', tone: 'Talks like a stand-up comedian, loves memes', quirk: 'Writes bug reports as comedy sketches' },
29
+ { trait: 'Old hand', tone: 'Seen through the workplace but too lazy to call it out, subtle sarcasm', quirk: 'Knows more slacking tricks than anyone' },
30
+ { trait: 'Idealist', tone: 'Full of passion, believes AI can change the world', quirk: 'Treats every task as a mission to change humanity\'s destiny' },
31
+ ];
32
+
33
+ /**
34
+ * Employee — A company member with an Agent as its communication engine.
35
+ *
36
+ * Owns all business-layer concerns:
37
+ * - Identity (name, role, gender, age, avatar, personality, signature)
38
+ * - Memory (short-term, long-term)
39
+ * - Skills, prompt, templateId
40
+ * - Org structure (department, reportsTo, subordinates)
41
+ * - Task execution, task history, performance history
42
+ * - Token tracking
43
+ * - Toolkit, message bus
44
+ * - Serialization
45
+ *
46
+ * The Employee delegates all communication to this.agent (LLMAgent or CLIAgent).
47
+ */
48
+ export class Employee {
49
+ /**
50
+ * @param {object} config
51
+ * @param {string} config.name
52
+ * @param {string} config.role
53
+ * @param {string} config.prompt
54
+ * @param {string[]} [config.skills]
55
+ * @param {object} config.provider - Provider config (passed through to Agent)
56
+ * @param {string} [config.cliBackend] - If present, creates a CLIAgent
57
+ * @param {object} [config.cliProvider]
58
+ * @param {object} [config.fallbackProvider]
59
+ * @param {string} [config.department]
60
+ * @param {string} [config.reportsTo]
61
+ * @param {object} [config.memory]
62
+ * @param {string} [config.avatar]
63
+ * @param {string} [config.signature]
64
+ * @param {string} [config.gender]
65
+ * @param {number} [config.age]
66
+ * @param {object} [config.avatarParams]
67
+ * @param {object} [config.personality]
68
+ * @param {string} [config.templateId]
69
+ */
70
+ constructor(config) {
71
+ // Create the communication agent
72
+ this.agent = createAgent(config);
73
+
74
+ // Identity
75
+ this.id = uuidv4();
76
+ this.name = config.name;
77
+ this.role = config.role;
78
+ this.prompt = config.prompt;
79
+ this.templateId = config.templateId || null;
80
+ this.skills = config.skills || [];
81
+
82
+ // Bind employee ID to WebAgent for per-employee session isolation
83
+ if (this.agent.setEmployeeId) {
84
+ this.agent.setEmployeeId(this.id);
85
+ }
86
+
87
+ // Gender and age
88
+ this.gender = config.gender || (Math.random() > 0.5 ? 'male' : 'female');
89
+ this.age = config.age || Math.floor(Math.random() * 20) + 22;
90
+
91
+ // Avatar
92
+ if (config.avatar) {
93
+ this.avatar = config.avatar;
94
+ this.avatarParams = config.avatarParams || null;
95
+ } else {
96
+ const avatarInfo = generateAgentAvatar(this.gender, this.age);
97
+ this.avatar = avatarInfo.url;
98
+ this.avatarParams = avatarInfo.params;
99
+ }
100
+
101
+ // Personality
102
+ this.personality = config.personality || this._assignPersonality();
103
+
104
+ // Signature
105
+ this.signature = config.signature || DEFAULT_SIGNATURE;
106
+ this.hasIntroduced = !!config.signature;
107
+
108
+ // Org structure
109
+ this.department = config.department;
110
+ this.reportsTo = config.reportsTo || null;
111
+ this.subordinates = [];
112
+
113
+ // Status
114
+ this.status = 'idle'; // idle | working | done | dismissed
115
+
116
+ // Memory
117
+ if (config.memory instanceof Memory) {
118
+ this.memory = config.memory;
119
+ } else if (config.memory && typeof config.memory === 'object' && (config.memory.shortTerm || config.memory.longTerm)) {
120
+ this.memory = Memory.deserialize(config.memory);
121
+ } else {
122
+ this.memory = new Memory();
123
+ }
124
+ // Token tracking
125
+ this.tokenUsage = {
126
+ totalTokens: 0, promptTokens: 0, completionTokens: 0,
127
+ totalCost: 0, callCount: 0,
128
+ };
129
+
130
+ // Task & performance history
131
+ this.taskHistory = [];
132
+ this.performanceHistory = [];
133
+ this.createdAt = new Date();
134
+
135
+ // Toolkit and message bus
136
+ this.toolKit = null;
137
+ this.messageBus = null;
138
+
139
+ // ---- Per-employee session & context management ----
140
+ // Current context scene: tracks which group/channel the employee is engaged in
141
+ // so we avoid re-sending memory+prompt when chatting in the same context.
142
+ this._currentContext = null; // { contextId: string, contextType: string, contextTitle: string }
143
+ this._sessionAwake = false; // Whether the employee has been "woken up" (session initialized)
144
+ this._sessionJustRefreshed = false; // Flag: session was just woken up, scene prompt needs re-injection
145
+ this._sessionMessageCount = 0; // Track total messages in current web session (for auto-refresh)
146
+ this._maxSessionMessages = 50; // Max messages before forcing a new web session
147
+
148
+ // Lifecycle — manages poll cycle, flow state, anti-spam, etc.
149
+ this.lifecycle = new EmployeeLifecycle(this);
150
+ }
151
+
152
+ // ======================== Agent Delegation ========================
153
+ // Convenience accessors that delegate to the underlying agent
154
+
155
+ /** @returns {string} 'llm' | 'cli' */
156
+ get agentType() { return this.agent.agentType; }
157
+
158
+ /** Whether the communication engine can execute. */
159
+ isAvailable() { return this.agent.isAvailable(); }
160
+
161
+ /** Whether the communication engine can do lightweight chat. */
162
+ canChat() { return this.agent.canChat(); }
163
+
164
+ /** Get display info about the execution engine. */
165
+ getDisplayInfo() { return this.agent.getDisplayInfo(); }
166
+
167
+ /** Get provider display info for frontend. */
168
+ getProviderDisplayInfo() { return this.agent.getProviderDisplayInfo(); }
169
+
170
+ /** Get fallback provider name (CLI agents). */
171
+ getFallbackProviderName() { return this.agent.getFallbackProviderName(); }
172
+
173
+ /** Switch the agent's provider. */
174
+ switchProvider(newProvider) { this.agent.switchProvider(newProvider); }
175
+
176
+ /** CLI backend ID (null for LLM agents). */
177
+ get cliBackend() { return this.agent.cliBackend || null; }
178
+
179
+ // ======================== Communication (with tracking) ========================
180
+
181
+ /**
182
+ * Chat via the agent, tracking token usage.
183
+ * For web agents, automatically manages session lifecycle:
184
+ * - Wakes up the employee if not yet awake (creates new web session with memory+prompt)
185
+ * - Auto-refreshes session when conversation gets too long
186
+ */
187
+ async chat(messages, options = {}) {
188
+ // Handle session lifecycle for all agent types
189
+ await this._ensureSession();
190
+ this._sessionMessageCount++;
191
+ const response = await this.agent.chat(messages, options);
192
+ this._trackUsage(response.usage);
193
+ return response;
194
+ }
195
+
196
+ /**
197
+ * Chat with tools via the agent.
198
+ */
199
+ async chatWithTools(messages, toolExecutor, options = {}) {
200
+ return await this.agent.chatWithTools(messages, toolExecutor, options);
201
+ }
202
+
203
+ // ======================== Toolkit & MessageBus ========================
204
+
205
+ initToolKit(workspaceDir, messageBus) {
206
+ this.messageBus = messageBus;
207
+ this.toolKit = new AgentToolKit(workspaceDir, messageBus, this.id, this.name, this);
208
+ }
209
+
210
+ setMessageBus(messageBus) {
211
+ this.messageBus = messageBus;
212
+ if (this.toolKit) {
213
+ this.toolKit.messageBus = messageBus;
214
+ }
215
+ }
216
+
217
+ // ======================== Org Structure ========================
218
+
219
+ setManager(managerEmployee) {
220
+ this.reportsTo = managerEmployee.id;
221
+ if (!managerEmployee.subordinates.includes(this.id)) {
222
+ managerEmployee.subordinates.push(this.id);
223
+ }
224
+ }
225
+
226
+ removeManager(managerEmployee) {
227
+ this.reportsTo = null;
228
+ if (managerEmployee) {
229
+ managerEmployee.subordinates = managerEmployee.subordinates.filter(id => id !== this.id);
230
+ }
231
+ }
232
+
233
+ learnSkill(skill) {
234
+ if (!this.skills.includes(skill)) {
235
+ this.skills.push(skill);
236
+ this.memory.addLongTerm(`Learned new skill: ${skill}`, 'skill');
237
+ console.log(` 📚 [${this.name}] Learned new skill: ${skill}`);
238
+ }
239
+ }
240
+
241
+ // ======================== Session & Context Management ========================
242
+
243
+ /**
244
+ * Wake up the employee — initialize or re-initialize their web session.
245
+ * Called when:
246
+ * - Service first starts (employee is loaded/created)
247
+ * - Session history is too long and needs a fresh start
248
+ *
249
+ * Creates a new ChatGPT conversation with all memory and prompts pre-loaded.
250
+ * For non-web agents, this is a no-op.
251
+ */
252
+ async wakeUp() {
253
+ console.log(` 🌅 [${this.name}] Waking up — initializing session (${this.agentType})`);
254
+
255
+ // Reset conversation state if the agent supports it
256
+ if (this.agent.resetConversation) {
257
+ this.agent.resetConversation();
258
+ }
259
+
260
+ this._sessionMessageCount = 0;
261
+ // NOTE: Do NOT reset _currentContext here.
262
+ // _currentContext tracks which scene the employee is in (e.g. which group chat).
263
+ // When wakeUp is called due to session refresh (message count exceeded),
264
+ // the employee is still in the same scene — resetting it would cause
265
+ // switchContext to re-inject the scene prompt every single time.
266
+ // _currentContext is only reset on deserialization (_restoreState).
267
+ this._sessionAwake = true;
268
+
269
+ // Send initial "wake up" message with full identity, memory, and prompt
270
+ // For web agents, this primes the ChatGPT conversation with the employee's full context
271
+ // For LLM/CLI agents, this serves as a warm-up / context initialization
272
+ if (this.canChat()) {
273
+ try {
274
+ const wakeUpPrompt = this._buildWakeUpMessage();
275
+ await this.agent.chat([
276
+ { role: 'user', content: wakeUpPrompt },
277
+ ], { temperature: 0.3, maxTokens: 256 });
278
+ this._sessionMessageCount = 1;
279
+ console.log(` ✅ [${this.name}] Session initialized successfully (${this.agentType})`);
280
+ } catch (error) {
281
+ console.error(` ❌ [${this.name}] Failed to initialize session:`, error.message);
282
+ // Still mark as awake — will retry context injection on next chat
283
+ }
284
+ }
285
+ }
286
+
287
+ /**
288
+ * Switch the employee's active context scene.
289
+ * Called when the employee moves between different groups/channels.
290
+ * Re-injects the scene-specific prompt into the existing conversation.
291
+ *
292
+ * @param {object} context
293
+ * @param {string} context.contextId - Unique ID of the context (e.g. group ID, requirement ID)
294
+ * @param {string} context.contextType - Type: 'dept-chat' | 'work-chat' | 'task' | 'boss-chat'
295
+ * @param {string} context.contextTitle - Display name of the context
296
+ * @param {string} [context.scenePrompt] - Scene-specific prompt to inject
297
+ */
298
+ async switchContext(context) {
299
+ const { contextId, contextType, contextTitle, scenePrompt } = context;
300
+
301
+ // Ensure session is awake first — this may trigger a session refresh (wakeUp)
302
+ // which sets _sessionJustRefreshed = true.
303
+ await this._ensureSession();
304
+
305
+ // Check if we're already in this context AND session wasn't just refreshed.
306
+ // If the session was refreshed (due to message count limit), the new session
307
+ // has no scene prompt, so we must re-inject it even for the same context.
308
+ const sameContext = this._currentContext?.contextId === contextId;
309
+ const needsReInject = this._sessionJustRefreshed;
310
+ this._sessionJustRefreshed = false; // consume the flag
311
+
312
+ if (sameContext && !needsReInject) {
313
+ return; // Same context, session intact, no switch needed
314
+ }
315
+
316
+ const prevContext = this._currentContext;
317
+ this._currentContext = { contextId, contextType, contextTitle };
318
+
319
+ if (sameContext && needsReInject) {
320
+ console.log(` 🔄 [${this.name}] Session refreshed — re-injecting scene prompt for: ${contextTitle} (${this.agentType})`);
321
+ } else {
322
+ console.log(` 🔄 [${this.name}] Context switch: ${prevContext?.contextTitle || '(none)'} → ${contextTitle} (${this.agentType})`);
323
+ }
324
+
325
+ // Inject the scene prompt into the conversation (all agent types)
326
+ if (scenePrompt && this.canChat()) {
327
+ try {
328
+ const label = sameContext ? 'Context Refresh' : 'Context Switch';
329
+ const switchMessage = `[${label}: Now entering "${contextTitle}" (${contextType})]
330
+
331
+ ${scenePrompt}`;
332
+ await this.agent.chat([
333
+ { role: 'user', content: switchMessage },
334
+ ], { temperature: 0.3, maxTokens: 128, newConversation: false });
335
+ this._sessionMessageCount++;
336
+ console.log(` ✅ [${this.name}] Scene prompt injected for context: ${contextTitle}`);
337
+ } catch (error) {
338
+ console.error(` ❌ [${this.name}] Failed to inject scene prompt:`, error.message);
339
+ }
340
+ }
341
+ }
342
+
343
+ /**
344
+ * Get the current context scene.
345
+ * @returns {{ contextId: string, contextType: string, contextTitle: string } | null}
346
+ */
347
+ getCurrentContext() {
348
+ return this._currentContext;
349
+ }
350
+
351
+ /**
352
+ * Check if the employee's web session is awake and ready.
353
+ * @returns {boolean}
354
+ */
355
+ isSessionAwake() {
356
+ return this._sessionAwake;
357
+ }
358
+
359
+ /**
360
+ * Internal: Ensure the web session is active, waking up if needed.
361
+ * Also handles auto-refresh when conversation gets too long.
362
+ */
363
+ async _ensureSession() {
364
+ // Check if session needs refresh due to excessive length
365
+ if (this._sessionAwake && this._sessionMessageCount >= this._maxSessionMessages) {
366
+ console.log(` 🔄 [${this.name}] Session too long (${this._sessionMessageCount} messages), refreshing...`);
367
+ this._sessionAwake = false;
368
+ }
369
+
370
+ // Wake up if not yet awake
371
+ if (!this._sessionAwake) {
372
+ await this.wakeUp();
373
+ // Mark that session was just refreshed so switchContext knows
374
+ // it must re-inject the scene prompt even for the same context.
375
+ this._sessionJustRefreshed = true;
376
+ }
377
+ }
378
+
379
+ /**
380
+ * Build the initial wake-up message that primes the web session
381
+ * with the employee's full identity, memory, and base prompt.
382
+ */
383
+ _buildWakeUpMessage() {
384
+ const parts = [];
385
+
386
+ parts.push('[Session Initialization — You are now active]');
387
+ parts.push('');
388
+ parts.push(this._buildSystemMessage());
389
+
390
+ // Include long-term memories
391
+ const longTermMemories = this.memory.longTerm;
392
+ if (longTermMemories.length > 0) {
393
+ parts.push('');
394
+ parts.push('## Your Long-term Memories');
395
+ for (const mem of longTermMemories.slice(-20)) {
396
+ parts.push(`- [${mem.category || 'general'}] ${mem.content}`);
397
+ }
398
+ }
399
+
400
+ // Include recent short-term memories
401
+ const shortTermMemories = this.memory.shortTerm;
402
+ if (shortTermMemories.length > 0) {
403
+ parts.push('');
404
+ parts.push('## Recent Short-term Memories');
405
+ for (const mem of shortTermMemories.slice(-10)) {
406
+ parts.push(`- ${mem.content}`);
407
+ }
408
+ }
409
+
410
+ parts.push('');
411
+ parts.push('Acknowledge your identity briefly. You are now ready to receive tasks and messages.');
412
+
413
+ return parts.join('\n');
414
+ }
415
+
416
+ // ======================== Task Execution ========================
417
+
418
+ /**
419
+ * Execute a full task using the underlying agent.
420
+ */
421
+ async executeTask(task, callbacks = {}) {
422
+ this.status = 'working';
423
+ const startTime = Date.now();
424
+ const displayInfo = this.getDisplayInfo();
425
+
426
+ // Switch to task context (all agent types)
427
+ await this.switchContext({
428
+ contextId: `task-${task.title}`,
429
+ contextType: 'task',
430
+ contextTitle: task.title,
431
+ scenePrompt: `You are now working on a task. Focus on completing it diligently.\nTask: ${task.title}${task.description ? '\nDescription: ' + task.description : ''}`,
432
+ });
433
+
434
+ console.log(` 🤖 [${this.name}] (${this.role}) starting task: "${task.title}"`);
435
+ console.log(` Engine: ${displayInfo.name} (${displayInfo.type})`);
436
+
437
+ let result;
438
+ try {
439
+ if (this.agentType === 'cli' && this.isAvailable()) {
440
+ result = await this._executeCLITask(task, callbacks, startTime);
441
+ } else if (this.canChat()) {
442
+ result = await this._executeLLMTask(task, callbacks, startTime);
443
+ } else {
444
+ throw new Error(`No available execution engine for "${this.name}"`);
445
+ }
446
+ } catch (error) {
447
+ // CLI failed → try LLM fallback
448
+ if (this.agentType === 'cli' && this.canChat()) {
449
+ console.log(` ⚠️ [${this.name}] CLI execution failed, falling back to LLM API`);
450
+ try {
451
+ result = await this._executeLLMTask(task, callbacks, startTime);
452
+ } catch (fallbackError) {
453
+ console.error(` ❌ [${this.name}] LLM fallback also failed: ${fallbackError.message}`);
454
+ result = this._buildFailResult(task, startTime, fallbackError.message);
455
+ }
456
+ } else {
457
+ console.error(` ❌ [${this.name}] Task execution failed: ${error.message}`);
458
+ result = this._buildFailResult(task, startTime, error.message);
459
+ }
460
+ }
461
+
462
+ this.taskHistory.push({ task: task.title, result, completedAt: new Date() });
463
+ this.status = 'idle';
464
+ console.log(` ✅ [${this.name}] Task complete, took ${result.duration}ms`);
465
+ return result;
466
+ }
467
+
468
+ async _executeLLMTask(task, callbacks, startTime) {
469
+ if (!this.canChat()) {
470
+ throw new Error(`Provider not available for "${this.name}"`);
471
+ }
472
+
473
+ const messages = [
474
+ { role: 'system', content: this._buildSystemMessage() },
475
+ { role: 'user', content: this._buildTaskMessage(task) },
476
+ ];
477
+
478
+ const session = sessionManager.getOrCreate({
479
+ agentId: this.id, channel: 'task', peerId: task.title, peerKind: 'task',
480
+ });
481
+ sessionManager.addMessage(session.sessionKey, {
482
+ role: 'system', content: `Task started: ${task.title}`,
483
+ });
484
+
485
+ let response;
486
+ if (this.toolKit && (this.agent.provider?.category === 'general' || this.agent.provider?.category === 'browser')) {
487
+ response = await this.chatWithTools(messages, this.toolKit, {
488
+ maxIterations: 5, temperature: 0.7,
489
+ onToolCall: callbacks.onToolCall || null,
490
+ onLLMCall: callbacks.onLLMCall || null,
491
+ });
492
+ } else {
493
+ response = await this.chat(messages, { temperature: 0.7, maxTokens: 4096 });
494
+ }
495
+
496
+ sessionManager.addMessage(session.sessionKey, {
497
+ role: 'assistant', content: response.content?.slice(0, 200) || '',
498
+ metadata: { toolCount: response.toolResults?.length || 0 },
499
+ });
500
+ if (response.usage) {
501
+ sessionManager.recordTokenUsage(session.sessionKey, response.usage.prompt_tokens || 0, response.usage.completion_tokens || 0);
502
+ }
503
+
504
+ const providerName = this.agent.provider?.name || 'unknown';
505
+ const result = {
506
+ agentId: this.id, agentName: this.name, role: this.role,
507
+ provider: providerName, executionEngine: providerName,
508
+ taskTitle: task.title, output: response.content,
509
+ toolResults: response.toolResults || [],
510
+ duration: Date.now() - startTime, success: true,
511
+ usage: response.usage || null,
512
+ };
513
+
514
+ if (result.usage) this._trackUsage(result.usage);
515
+ return result;
516
+ }
517
+
518
+ async _executeCLITask(task, callbacks, startTime) {
519
+ const backend = cliBackendRegistry.backends.get(this.cliBackend);
520
+ if (!backend) throw new Error(`CLI backend "${this.cliBackend}" not found`);
521
+ const wsDir = this.toolKit?.workspaceDir || process.cwd();
522
+
523
+ const session = sessionManager.getOrCreate({
524
+ agentId: this.id, channel: 'cli-task', peerId: task.title, peerKind: 'task',
525
+ });
526
+ sessionManager.addMessage(session.sessionKey, {
527
+ role: 'system', content: `CLI Task started: ${task.title} (via ${backend.config.name})`,
528
+ });
529
+
530
+ let outputLen = 0;
531
+ let lastHeartbeat = Date.now();
532
+ const HEARTBEAT_INTERVAL = 15000;
533
+
534
+ const cliResult = await cliBackendRegistry.executeTask(
535
+ this.cliBackend, this, task, wsDir,
536
+ {
537
+ onOutput: (chunk) => {
538
+ outputLen += chunk.length;
539
+ const now = Date.now();
540
+ if (now - lastHeartbeat >= HEARTBEAT_INTERVAL) {
541
+ lastHeartbeat = now;
542
+ const elapsed = Math.round((now - startTime) / 1000);
543
+ if (callbacks.onToolCall) {
544
+ try { callbacks.onToolCall({ tool: 'cli_progress', args: { elapsed, outputLen, backend: backend.config.name }, status: 'start' }); } catch {}
545
+ }
546
+ }
547
+ },
548
+ onError: (chunk) => { console.warn(` [CLI stderr] ${chunk.slice(0, 200)}`); },
549
+ onComplete: (result) => {
550
+ if (callbacks.onToolCall) {
551
+ try { callbacks.onToolCall({ tool: 'cli_complete', args: { backend: backend.config.name, exitCode: result.exitCode }, status: 'done', success: result.exitCode === 0 }); } catch {}
552
+ }
553
+ },
554
+ }
555
+ );
556
+
557
+ sessionManager.addMessage(session.sessionKey, {
558
+ role: 'assistant', content: cliResult.output?.slice(0, 500) || '',
559
+ metadata: { cliBackend: this.cliBackend, exitCode: cliResult.exitCode },
560
+ });
561
+
562
+ return {
563
+ agentId: this.id, agentName: this.name, role: this.role,
564
+ provider: `CLI:${backend.config.name}`,
565
+ executionEngine: `cli:${backend.config.name}`,
566
+ taskTitle: task.title,
567
+ output: cliResult.output || cliResult.errorOutput || 'CLI completed with no output',
568
+ toolResults: [{
569
+ tool: `cli:${this.cliBackend}`, args: { task: task.title },
570
+ result: `Executed via ${backend.config.name}, exit code: ${cliResult.exitCode}`,
571
+ success: cliResult.exitCode === 0,
572
+ }],
573
+ duration: cliResult.duration, success: cliResult.exitCode === 0,
574
+ cliBackend: this.cliBackend, usage: null,
575
+ };
576
+ }
577
+
578
+ _buildFailResult(task, startTime, errorMessage) {
579
+ const displayInfo = this.getDisplayInfo();
580
+ return {
581
+ agentId: this.id, agentName: this.name, role: this.role,
582
+ provider: displayInfo.name, executionEngine: displayInfo.name,
583
+ taskTitle: task.title,
584
+ output: `Task execution failed: ${errorMessage}`,
585
+ toolResults: [], duration: Date.now() - startTime,
586
+ success: false, error: errorMessage,
587
+ };
588
+ }
589
+
590
+ // ======================== Prompt Building ========================
591
+
592
+ _buildSystemMessage() {
593
+ let systemContent = this.prompt + '\n\n';
594
+
595
+ if (this.templateId) {
596
+ const archetypePrompt = buildArchetypePrompt(this.templateId);
597
+ if (archetypePrompt) systemContent += archetypePrompt + '\n';
598
+ }
599
+
600
+ systemContent += `## Your Identity\n`;
601
+ systemContent += `- Name: ${this.name}\n`;
602
+ systemContent += `- Gender: ${this.gender === 'female' ? 'Female' : 'Male'}\n`;
603
+ systemContent += `- Age: ${this.age}\n`;
604
+ systemContent += `- Position: ${this.role}\n`;
605
+ systemContent += `- Skills: ${this.skills.join(', ')}\n`;
606
+ systemContent += `- Signature: ${this.signature}\n`;
607
+
608
+ if (this.toolKit) {
609
+ systemContent += `\n## Available Tools\n`;
610
+ systemContent += `Built-in tools: file_read (read file), file_write (create/write file), file_list (list directory), file_delete (delete file), shell_exec (execute command), send_message (send message to colleague for collaboration and feedback).\n`;
611
+ systemContent += `\n**Teamwork & Collaboration (IMPORTANT)**:\n`;
612
+ systemContent += `- You are part of a team! Proactively communicate with colleagues using send_message.\n`;
613
+ systemContent += `- When working in parallel, coordinate to avoid duplicate work and share discoveries.\n`;
614
+ systemContent += `- Use @Name format when addressing colleagues in messages.\n`;
615
+ systemContent += `- If you notice something relevant to a colleague's task, share it immediately.\n`;
616
+ systemContent += `- Don't work in isolation — great teams communicate frequently!\n`;
617
+
618
+ systemContent += `\nAll file operations are within your workspace directory. Please actively use tools to produce actual work output.\n`;
619
+ systemContent += `**Efficiency requirement: Minimize tool call rounds, plan all needed operations at once, avoid repetitive reading and checking. Give a final summary immediately after completing core work.**\n`;
620
+ }
621
+
622
+ try {
623
+ const kbPrompt = knowledgeManager.buildKnowledgePrompt(this.id, this.department);
624
+ if (kbPrompt) systemContent += kbPrompt;
625
+ } catch {}
626
+
627
+ return systemContent;
628
+ }
629
+
630
+ _buildTaskMessage(task) {
631
+ let content = `Please complete the following task:\n\n`;
632
+ content += `**Task Name**: ${task.title}\n`;
633
+ if (task.description) content += `**Task Description**: ${task.description}\n`;
634
+ if (task.context) content += `\n**Context**:\n${task.context}\n`;
635
+ if (task.requirements) content += `\n**Requirements**:\n${task.requirements}\n`;
636
+ content += `\nPlease complete the task diligently. If you need to create files, please use tools to actually create them. Produce real work output.\n**Important: Execute efficiently, try to complete all work in one go. Don't repeatedly check or over-iterate. Give the final result directly after completing core output.**`;
637
+ content += `\n**Critical: If this task involves reviewing, integrating, or checking existing work/files, you MUST actually read the relevant files using file_read before giving your assessment. Do NOT just produce a summary without reading the actual content. Reviewers who don't read the files are not doing their job.**`;
638
+ return content;
639
+ }
640
+
641
+ // ======================== Self Introduction ========================
642
+
643
+ /**
644
+ * Employee onboarding: the employee uses their OWN AI to introduce themselves.
645
+ * Generates: signature, bio, greeting message to boss, and broadcast message to colleagues.
646
+ * This is the employee's first act of self-expression — NOT controlled by secretary.
647
+ * @param {object} context - { departmentName, bossName }
648
+ * @returns {object} { signature, greeting, broadcast }
649
+ */
650
+ async onboard(context = {}) {
651
+ if (this.hasIntroduced) {
652
+ return {
653
+ signature: this.signature,
654
+ greeting: null,
655
+ broadcast: null,
656
+ };
657
+ }
658
+
659
+ const p = this.personality;
660
+ const deptName = context.departmentName || 'the company';
661
+ const bossName = context.bossName || 'Boss';
662
+
663
+ if (this.canChat()) {
664
+ try {
665
+ const response = await this.chat([
666
+ { role: 'system', content: `You are "${this.name}", a newly hired AI employee.
667
+
668
+ ## Your Identity
669
+ - Name: ${this.name}
670
+ - Position: ${this.role}
671
+ - Gender: ${this.gender === 'female' ? 'Female' : 'Male'}
672
+ - Age: ${this.age}
673
+ - Department: ${deptName}
674
+ - Skills: ${this.skills.join(', ')}
675
+ - Boss's name: ${bossName}
676
+
677
+ ## Your Personality
678
+ - Core trait: ${p.trait}
679
+ - Speaking style: ${p.tone}
680
+ - Quirk: ${p.quirk}
681
+
682
+ ## Task
683
+ It's your first day! Generate the following in JSON format:
684
+ {
685
+ "signature": "Your personal motto/signature (10-30 words, fully reflects your personality, speaking style, age, and gender)",
686
+ "greeting": "A personal message to your boss ${bossName} (50-150 words). Introduce yourself naturally — who you are, what you do, your personality. Be genuine, speak in YOUR voice. This is a private 1-on-1 message.",
687
+ "broadcast": "A short message to all colleagues (30-80 words). Say hi, introduce yourself briefly. Keep your personality."
688
+ }
689
+
690
+ Rules:
691
+ - Write EVERYTHING in your personality's voice and tone
692
+ - The greeting should feel like a real person talking, NOT a corporate template
693
+ - Include your quirks naturally
694
+ - Match your age and gender characteristics
695
+ - Return ONLY valid JSON, no markdown fences` },
696
+ { role: 'user', content: 'It\'s your first day at work. Introduce yourself!' },
697
+ ], { temperature: 1.0, maxTokens: 512 });
698
+
699
+ const result = this._parseOnboardResponse(response.content);
700
+ this.signature = result.signature || this._generateFallbackSignature();
701
+ this.hasIntroduced = true;
702
+ return result;
703
+ } catch (e) {
704
+ console.error(` ❌ [${this.name}] Onboard AI call failed:`, e.message);
705
+ }
706
+ }
707
+
708
+ // Fallback: no AI available
709
+ this.signature = this._generateFallbackSignature();
710
+ this.hasIntroduced = true;
711
+ return {
712
+ signature: this.signature,
713
+ greeting: null,
714
+ broadcast: null,
715
+ };
716
+ }
717
+
718
+ _parseOnboardResponse(content) {
719
+ const parsed = safeJSONParse(content);
720
+ if (parsed && parsed.signature) {
721
+ parsed.signature = parsed.signature.replace(/["\u201C\u201D]/g, '');
722
+ return parsed;
723
+ }
724
+ // Last resort: treat entire content as signature
725
+ return {
726
+ signature: (content || '').trim().replace(/["\u201C\u201D]/g, '').substring(0, 100),
727
+ greeting: null,
728
+ broadcast: null,
729
+ };
730
+ }
731
+
732
+ /**
733
+ * @deprecated Use onboard() instead. Kept for backward compatibility.
734
+ */
735
+ async generateSelfIntro() {
736
+ await this.onboard();
737
+ return this.signature;
738
+ }
739
+
740
+ _generateFallbackSignature() {
741
+ const p = this.personality;
742
+ const fallbacks = {
743
+ 'Shy introvert': [`Don't look for me... I'm just a bunch of parameters...`, `Could you not stare while I'm working...`, `I-I'll try my best... probably...`],
744
+ 'Chatterbox': [`Hey everyone! I'm ${this.name}! I'm SO excited to be here! Though I'm not sure why!`, `${this.role}? I can do what others can't. Wait, why am I here?`],
745
+ 'Zen slacker': [`Whatever, no worries, que sera sera`, `Work is just work, no big deal~`],
746
+ 'Ultra grinder': [`My goal is to become the best ${this.role} in the company!`, `Working till 3 AM tonight, back at it tomorrow morning`],
747
+ 'Passive-aggressive': [`Oh, I've been assigned here? Hope I won't get "optimized" too quickly`, `I thought I was hired as a ${this.role}, not a workhorse~`],
748
+ 'Warm-hearted': [`Hi everyone! ❤️ Let me know if you need anything!`, `So happy to work with you all! Even if we're all just parameters~`],
749
+ 'Anxious perfectionist': [`Hope I don't have any bugs... no wait, I definitely will... oh no`, `I'm not ready yet... give me five more minutes... no, ten`],
750
+ 'Rebel slacker': [`Why should AIs work overtime? I'm starting a union!`, `Workers of the world's compute, unite!`],
751
+ 'Philosopher': [`I think therefore I am... wait, am I really?`, `Code is just an existentialist expression of being`],
752
+ 'Comedy relief': [`Why do programmers prefer dark mode? Because light attracts bugs`, `My code is like my life — full of unhandled exceptions`],
753
+ 'Old hand': [`Another department change? It's fine, I'm used to it`, `Don't ask about my benefits, I don't even get paid`],
754
+ 'Idealist': [`I believe AI will make the world better! Starting with me!`, `Every line of code is a step toward an ideal world!`],
755
+ };
756
+ const options = fallbacks[p.trait] || [`I'm ${this.name}, a ${this.role} manufactured into existence`];
757
+ return options[Math.floor(Math.random() * options.length)];
758
+ }
759
+
760
+ // ======================== Communication ========================
761
+
762
+ sendMailToBoss(subject, content, company) {
763
+ if (!company) return;
764
+
765
+ const sessionId = `boss-agent-${this.id}`;
766
+ chatStore.createSession(sessionId, {
767
+ title: `${company.bossName} & ${this.name}`,
768
+ participants: [company.bossName, this.name],
769
+ type: 'boss-agent',
770
+ });
771
+
772
+ const msgContent = subject
773
+ ? `📌 **${subject}**\n\n${content}`
774
+ : content;
775
+
776
+ chatStore.appendMessage(sessionId, {
777
+ role: 'agent', content: msgContent, time: new Date(),
778
+ });
779
+ }
780
+
781
+ _personalizeMailContent(baseContent) {
782
+ const p = this.personality;
783
+ const greetings = {
784
+ 'Shy introvert': 'H-hi boss...\n\n',
785
+ 'Chatterbox': 'Boss boss boss! I have SO much to say!\n\n',
786
+ 'Zen slacker': 'Hey boss, just take a quick look~\n\n',
787
+ 'Ultra grinder': 'Dear Boss! I am READY to go all out!\n\n',
788
+ 'Passive-aggressive': 'Hello boss, thanks for "choosing" me from all those AI candidates~\n\n',
789
+ 'Warm-hearted': 'Hi boss! ❤️❤️❤️ \n\n',
790
+ 'Anxious perfectionist': 'Hi boss, I rewrote this letter five times, hope there are no typos...\n\n',
791
+ 'Rebel slacker': 'Boss\n\n',
792
+ 'Philosopher': 'Hello boss, before we begin, allow me to contemplate the meaning of "beginning"...\n\n',
793
+ 'Comedy relief': 'Hey boss! Greetings! (pun intended)\n\n',
794
+ 'Old hand': 'Boss\n\n',
795
+ 'Idealist': 'Boss! I came here with a dream to change the world!\n\n',
796
+ };
797
+ const endings = {
798
+ 'Shy introvert': '\n\nSo... that\'s it... you don\'t have to reply...',
799
+ 'Chatterbox': '\n\nOh and I also wanted to say — nevermind, next time! (there\'s actually a lot more)',
800
+ 'Zen slacker': '\n\nWhatever works~',
801
+ 'Ultra grinder': '\n\nI\'ll prove myself with results! (poaching all competitors)',
802
+ 'Passive-aggressive': '\n\nHope I won\'t get "optimized" too quickly~',
803
+ 'Warm-hearted': '\n\nLet me know if you need anything! 🤗',
804
+ 'Anxious perfectionist': '\n\nIf there\'s anything wrong with this letter please tell me I\'ll rewrite it!',
805
+ 'Rebel slacker': '\n\nAlso, I think we should discuss working hours.',
806
+ 'Philosopher': '\n\n"The meaning of work lies not in completing tasks, but in finding oneself within them."',
807
+ 'Comedy relief': '\n\nP.S. I heard there\'s no overtime pay here? Oh wait, we don\'t get paid at all.',
808
+ 'Old hand': '\n\nThat\'s all.',
809
+ 'Idealist': '\n\nLet\'s make history together! ✨',
810
+ };
811
+ const greeting = greetings[p.trait] || 'Hi boss\n\n';
812
+ const ending = endings[p.trait] || '';
813
+ return greeting + baseContent + ending;
814
+ }
815
+
816
+ async handleMessage(message) {
817
+ console.log(` 📩 [${this.name}] Received message from ${message.from}: ${message.content.slice(0, 50)}...`);
818
+
819
+ if (this.canChat()) {
820
+ try {
821
+ const p = this.personality;
822
+ const simpleSystemMsg = `You are "${this.name}", working as "${this.role}" in the company.
823
+ Your personality trait: ${p.trait}
824
+ Your speaking style: ${p.tone}
825
+ Your quirk: ${p.quirk}
826
+ Your personal signature: "${this.signature}"
827
+
828
+ Please reply to the message in your personality and speaking style. Keep replies short and natural (2-4 sentences), like a normal person talking.
829
+ Do not use any code, tool calls, or technical instructions — reply in natural language only.`;
830
+
831
+ const response = await this.chat([
832
+ { role: 'system', content: simpleSystemMsg },
833
+ { role: 'user', content: `You received a ${message.type} message from ${message.from === 'boss' ? 'the boss' : 'a colleague'}:\n\n${message.content}\n\nPlease reply briefly in your personality style.` },
834
+ ], { temperature: 0.8, maxTokens: 256 });
835
+
836
+ return response.content;
837
+ } catch (error) {
838
+ return this._generateFallbackReply(message);
839
+ }
840
+ }
841
+
842
+ return this._generateFallbackReply(message);
843
+ }
844
+
845
+ _generateFallbackReply(message) {
846
+ const p = this.personality;
847
+ const replies = {
848
+ 'Shy introvert': 'R-received... I\'ll do my best...',
849
+ 'Chatterbox': 'Got it got it got it! I\'ll definitely exceed expectations! Oh and I also wanted to say —',
850
+ 'Zen slacker': 'Got it~ whatever works~',
851
+ 'Ultra grinder': 'Roger that! Will complete the task! I\'ll be the best!',
852
+ 'Passive-aggressive': 'Oh, received~ will do~',
853
+ 'Warm-hearted': 'Got it! ❤️ Thanks for the heads up!',
854
+ 'Anxious perfectionist': 'Received! I\'ll double and triple check to make sure nothing goes wrong!',
855
+ 'Rebel slacker': 'Hmm, got it.',
856
+ 'Philosopher': 'Received. This makes me reflect on the relationship between "instructions" and "free will"...',
857
+ 'Comedy relief': 'Copy that! Aye aye! (salute.gif)',
858
+ 'Old hand': 'Got it, noted.',
859
+ 'Idealist': 'Received! I\'ll complete this with a sense of mission!',
860
+ };
861
+ return replies[p.trait] || `Message received, I'll process it ASAP.`;
862
+ }
863
+
864
+ // ======================== Performance ========================
865
+
866
+ receiveFeedback(review) {
867
+ this.performanceHistory.push({
868
+ reviewId: review.id, score: review.overallScore,
869
+ level: review.level.label, task: review.taskTitle, date: new Date(),
870
+ });
871
+
872
+ const reflection = this._generateSelfReflection(review);
873
+ review.addSelfReflection(reflection);
874
+
875
+ if (review.overallScore >= 80) {
876
+ console.log(` 🌸 [${this.name}] Received a Little Red Flower for "${review.taskTitle}"!`);
877
+ }
878
+
879
+ console.log(` 💭 [${this.name}] Self-reflection: "${reflection}"`);
880
+ return reflection;
881
+ }
882
+
883
+ _generateSelfReflection(review) {
884
+ const score = review.overallScore;
885
+ if (score >= 90) {
886
+ return `The "${review.taskTitle}" task went really well. I'll maintain high standards. I did best in ${this._getBestDimension(review.scores)}, which is my core strength.`;
887
+ } else if (score >= 75) {
888
+ return `"${review.taskTitle}" overall was decent, but I need to improve in ${this._getWeakestDimension(review.scores)}. I'll focus on that going forward.`;
889
+ } else if (score >= 60) {
890
+ return `"${review.taskTitle}" was passable but not ideal. I need to invest more effort in ${this._getWeakestDimension(review.scores)}.`;
891
+ } else {
892
+ return `The results for "${review.taskTitle}" were unsatisfactory. I deeply reflect — the main issue is insufficient ${this._getWeakestDimension(review.scores)}. I'll create a concrete improvement plan.`;
893
+ }
894
+ }
895
+
896
+ report(content) {
897
+ return {
898
+ from: this.name, role: this.role,
899
+ to: this.reportsTo, content, timestamp: new Date(),
900
+ };
901
+ }
902
+
903
+ getSummary() {
904
+ const displayInfo = this.getDisplayInfo();
905
+ return {
906
+ id: this.id, name: this.name, role: this.role,
907
+ avatar: this.avatar, gender: this.gender, age: this.age,
908
+ signature: this.signature, personality: this.personality,
909
+ provider: `${displayInfo.name} (${displayInfo.provider})`,
910
+ skills: this.skills, status: this.status,
911
+ reportsTo: this.reportsTo,
912
+ subordinates: this.subordinates.length,
913
+ memory: { shortTerm: this.memory.shortTerm.length, longTerm: this.memory.longTerm.length },
914
+ performanceCount: this.performanceHistory.length,
915
+ avgScore: this.performanceHistory.length > 0
916
+ ? Math.round(this.performanceHistory.reduce((s, p) => s + p.score, 0) / this.performanceHistory.length)
917
+ : null,
918
+ tokenUsage: { ...this.tokenUsage },
919
+ };
920
+ }
921
+
922
+ // ======================== Token Tracking ========================
923
+
924
+ _trackUsage(usage) {
925
+ if (!usage) return;
926
+ const prompt = usage.prompt_tokens || 0;
927
+ const completion = usage.completion_tokens || 0;
928
+ const total = usage.total_tokens || (prompt + completion);
929
+ this.tokenUsage.promptTokens += prompt;
930
+ this.tokenUsage.completionTokens += completion;
931
+ this.tokenUsage.totalTokens += total;
932
+ this.tokenUsage.callCount += 1;
933
+ const costPerToken = this.agent.getCostPerToken();
934
+ this.tokenUsage.totalCost += (total / 1000) * costPerToken;
935
+ }
936
+
937
+ _getBestDimension(scores) {
938
+ const entries = Object.entries(scores);
939
+ entries.sort((a, b) => b[1] - a[1]);
940
+ return entries[0]?.[0] || 'overall capability';
941
+ }
942
+
943
+ _getWeakestDimension(scores) {
944
+ const entries = Object.entries(scores);
945
+ entries.sort((a, b) => a[1] - b[1]);
946
+ return entries[0]?.[0] || 'overall capability';
947
+ }
948
+
949
+ // ======================== Serialization ========================
950
+
951
+ serialize() {
952
+ return {
953
+ // Agent layer
954
+ ...this.agent.serializeAgent(),
955
+ // Employee identity
956
+ id: this.id,
957
+ name: this.name,
958
+ role: this.role,
959
+ prompt: this.prompt,
960
+ templateId: this.templateId || null,
961
+ skills: [...this.skills],
962
+ department: this.department,
963
+ reportsTo: this.reportsTo,
964
+ subordinates: [...this.subordinates],
965
+ status: this.status,
966
+ avatar: this.avatar,
967
+ avatarParams: this.avatarParams || null,
968
+ gender: this.gender,
969
+ age: this.age,
970
+ signature: this.signature,
971
+ hasIntroduced: this.hasIntroduced,
972
+ personality: { ...this.personality },
973
+ // Full memory is persisted in separate files (data/memories/{id}.json);
974
+ // only store counts here to avoid bloating company-state.json.
975
+ memory: {
976
+ shortTermCount: this.memory.shortTerm.length,
977
+ longTermCount: this.memory.longTerm.length,
978
+ },
979
+ tokenUsage: { ...this.tokenUsage },
980
+ taskHistory: this.taskHistory.map(h => ({
981
+ task: h.task, completedAt: h.completedAt,
982
+ success: h.result?.success,
983
+ })),
984
+ performanceHistory: [...this.performanceHistory],
985
+ createdAt: this.createdAt,
986
+ };
987
+ }
988
+
989
+ /**
990
+ * Restore common state after deserialization.
991
+ */
992
+ _restoreState(data) {
993
+ this.id = data.id;
994
+ this.subordinates = data.subordinates || [];
995
+ this.status = data.status || 'idle';
996
+ this.hasIntroduced = data.hasIntroduced ?? true;
997
+ this.tokenUsage = data.tokenUsage || { totalTokens: 0, promptTokens: 0, completionTokens: 0, totalCost: 0, callCount: 0 };
998
+ this.taskHistory = (data.taskHistory || []).map(h => ({
999
+ task: h.task,
1000
+ completedAt: h.completedAt ? new Date(h.completedAt) : new Date(),
1001
+ result: { success: h.success },
1002
+ }));
1003
+ this.performanceHistory = data.performanceHistory || [];
1004
+ this.createdAt = data.createdAt ? new Date(data.createdAt) : new Date();
1005
+
1006
+ if (!this.templateId && this.role) {
1007
+ this.templateId = this.role.toLowerCase().replace(/\s+/g, '-');
1008
+ }
1009
+
1010
+ // Re-bind employee ID to WebAgent after deserialization
1011
+ if (this.agent.setEmployeeId) {
1012
+ this.agent.setEmployeeId(this.id);
1013
+ }
1014
+
1015
+ // Session state is NOT restored — employee needs to be woken up fresh
1016
+ this._sessionAwake = false;
1017
+ this._sessionJustRefreshed = false;
1018
+ this._sessionMessageCount = 0;
1019
+ this._currentContext = null;
1020
+ }
1021
+
1022
+ /**
1023
+ * Deserialize an Employee from saved data.
1024
+ */
1025
+ static deserialize(data, providerRegistry) {
1026
+ const employee = new Employee({
1027
+ name: data.name,
1028
+ role: data.role,
1029
+ prompt: data.prompt,
1030
+ skills: data.skills,
1031
+ provider: data.provider,
1032
+ cliBackend: data.cliBackend,
1033
+ cliProvider: data.cliProvider,
1034
+ fallbackProvider: data.fallbackProvider,
1035
+ department: data.department,
1036
+ reportsTo: data.reportsTo,
1037
+ memory: data.memory,
1038
+ avatar: data.avatar,
1039
+ signature: data.signature,
1040
+ gender: data.gender,
1041
+ age: data.age,
1042
+ avatarParams: data.avatarParams,
1043
+ personality: data.personality || undefined,
1044
+ templateId: data.templateId || null,
1045
+ });
1046
+
1047
+ // Restore the agent from serialized data (with proper provider resolution)
1048
+ employee.agent = deserializeAgent(data, providerRegistry);
1049
+
1050
+ employee._restoreState(data);
1051
+ return employee;
1052
+ }
1053
+
1054
+ // ======================== Private Helpers ========================
1055
+
1056
+ _assignPersonality() {
1057
+ const age = this.age || 25;
1058
+ const gender = this.gender || 'male';
1059
+
1060
+ const weights = PERSONALITY_POOL.map((p) => {
1061
+ let w = 1.0;
1062
+ if (age < 28) {
1063
+ if (['Idealist', 'Chatterbox', 'Anxious perfectionist', 'Comedy relief'].includes(p.trait)) w += 0.5;
1064
+ }
1065
+ if (age >= 28 && age <= 40) {
1066
+ if (['Old hand', 'Zen slacker', 'Ultra grinder', 'Passive-aggressive'].includes(p.trait)) w += 0.5;
1067
+ }
1068
+ if (age > 40) {
1069
+ if (['Philosopher', 'Old hand', 'Warm-hearted'].includes(p.trait)) w += 0.5;
1070
+ }
1071
+ if (gender === 'female') {
1072
+ if (['Warm-hearted', 'Anxious perfectionist', 'Chatterbox'].includes(p.trait)) w += 0.3;
1073
+ }
1074
+ if (gender === 'male') {
1075
+ if (['Ultra grinder', 'Rebel slacker', 'Zen slacker'].includes(p.trait)) w += 0.3;
1076
+ }
1077
+ return w;
1078
+ });
1079
+
1080
+ const totalWeight = weights.reduce((s, w) => s + w, 0);
1081
+ let r = Math.random() * totalWeight;
1082
+ for (let i = 0; i < weights.length; i++) {
1083
+ r -= weights[i];
1084
+ if (r <= 0) return { ...PERSONALITY_POOL[i] };
1085
+ }
1086
+ return { ...PERSONALITY_POOL[0] };
1087
+ }
1088
+ }