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,2600 @@
|
|
|
1
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { ProviderRegistry, ModelProviders, JobCategory, JobCategoryLabel } from './workforce/providers.js';
|
|
4
|
+
import { HRSystem, JobTemplates } from './workforce/hr.js';
|
|
5
|
+
import { Department } from './department.js';
|
|
6
|
+
import { Employee, createEmployee, deserializeEmployee, Secretary } from '../employee/index.js';
|
|
7
|
+
import { LLMAgent, CLIAgent, WebAgent } from '../agent/index.js';
|
|
8
|
+
import { PerformanceSystem } from '../employee/performance.js';
|
|
9
|
+
import { TalentMarket } from './workforce/talent-market.js';
|
|
10
|
+
import { MessageBus } from '../agent/message-bus.js';
|
|
11
|
+
import { existsSync, mkdirSync } from 'fs';
|
|
12
|
+
import { WorkspaceManager } from '../workspace.js';
|
|
13
|
+
import { debouncedSave } from './persistence.js';
|
|
14
|
+
import { llmClient } from '../agent/llm-agent/client.js';
|
|
15
|
+
import { webClientRegistry } from '../agent/web-agent/web-client.js';
|
|
16
|
+
import { loadAgentMemory, saveAgentMemory } from '../employee/memory/store.js';
|
|
17
|
+
import { Memory } from '../employee/memory/index.js';
|
|
18
|
+
import { RequirementManager, RequirementStatus } from '../requirement.js';
|
|
19
|
+
import { TeamManager, SprintStatus } from './team.js';
|
|
20
|
+
import { hookRegistry, HookEvent } from '../../lib/hooks.js';
|
|
21
|
+
import { sessionManager } from '../agent/session.js';
|
|
22
|
+
import { robustJSONParse } from '../utils/json-parse.js';
|
|
23
|
+
import { cronScheduler } from '../system/cron.js';
|
|
24
|
+
import { pluginRegistry } from '../system/plugin.js';
|
|
25
|
+
import { auditLogger, AuditCategory, AuditLevel } from '../system/audit.js';
|
|
26
|
+
import { chatStore } from '../agent/chat-store.js';
|
|
27
|
+
import { cliBackendRegistry } from '../agent/cli-agent/backends/index.js';
|
|
28
|
+
import { groupChatLoop } from './group-chat-loop.js';
|
|
29
|
+
|
|
30
|
+
// Expand short-form file references: [[file:path]] → [[file:deptId:path|name]]
|
|
31
|
+
// Also fix incomplete references: [[file:deptId:path]] → [[file:deptId:path|name]]
|
|
32
|
+
// Only creates clickable references for files that actually exist on disk.
|
|
33
|
+
// Returns { content, invalidRefs } so caller can provide feedback for bad references.
|
|
34
|
+
const SIMPLE_FILE_REF = /\[\[file:([^\]|:]+)\]\]/g;
|
|
35
|
+
const INCOMPLETE_FILE_REF = /\[\[file:([^:]+):([^\]|]+)\]\]/g;
|
|
36
|
+
function expandFileReferences(content, departmentId, workspacePath) {
|
|
37
|
+
if (!content || !departmentId) return { content, invalidRefs: [] };
|
|
38
|
+
const invalidRefs = [];
|
|
39
|
+
// First: fix incomplete refs [[file:deptId:path]] → [[file:deptId:path|name]]
|
|
40
|
+
let expanded = content.replace(INCOMPLETE_FILE_REF, (_match, deptId, filePath) => {
|
|
41
|
+
const trimmed = filePath.trim();
|
|
42
|
+
if (workspacePath) {
|
|
43
|
+
const fullPath = path.join(workspacePath, trimmed);
|
|
44
|
+
if (!existsSync(fullPath)) {
|
|
45
|
+
invalidRefs.push(trimmed);
|
|
46
|
+
return trimmed;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
const displayName = path.basename(trimmed);
|
|
50
|
+
return `[[file:${deptId}:${trimmed}|${displayName}]]`;
|
|
51
|
+
});
|
|
52
|
+
// Then: expand simple refs [[file:path]] → [[file:deptId:path|name]]
|
|
53
|
+
expanded = expanded.replace(SIMPLE_FILE_REF, (_match, filePath) => {
|
|
54
|
+
const trimmed = filePath.trim();
|
|
55
|
+
if (workspacePath) {
|
|
56
|
+
const fullPath = path.join(workspacePath, trimmed);
|
|
57
|
+
if (!existsSync(fullPath)) {
|
|
58
|
+
invalidRefs.push(trimmed);
|
|
59
|
+
return trimmed;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
const displayName = path.basename(trimmed);
|
|
63
|
+
return `[[file:${departmentId}:${trimmed}|${displayName}]]`;
|
|
64
|
+
});
|
|
65
|
+
return { content: expanded, invalidRefs };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Company - AI Enterprise
|
|
71
|
+
* Integrates message bus and workspace management, enabling Agents to actually perform work
|
|
72
|
+
*/
|
|
73
|
+
export class Company {
|
|
74
|
+
constructor(companyName, bossName = 'Boss', secretaryConfig = null) {
|
|
75
|
+
this.id = uuidv4();
|
|
76
|
+
this.name = companyName;
|
|
77
|
+
this.bossName = bossName;
|
|
78
|
+
this.bossAvatar = null; // Boss avatar URL
|
|
79
|
+
this.departments = new Map();
|
|
80
|
+
this.providerRegistry = new ProviderRegistry();
|
|
81
|
+
// Sync CLI backends into provider registry so they appear in Brain Providers
|
|
82
|
+
this.providerRegistry.syncCLIBackends(cliBackendRegistry);
|
|
83
|
+
// Auto-detect CLI backends in background (async, fire-and-forget)
|
|
84
|
+
cliBackendRegistry.detectAll().then(() => {
|
|
85
|
+
this.providerRegistry.syncCLIBackends(cliBackendRegistry);
|
|
86
|
+
}).catch(() => {});
|
|
87
|
+
this.talentMarket = new TalentMarket();
|
|
88
|
+
this.performanceSystem = new PerformanceSystem();
|
|
89
|
+
this.hr = new HRSystem(this.providerRegistry, this.talentMarket);
|
|
90
|
+
this.logs = [];
|
|
91
|
+
// Chat history with secretary
|
|
92
|
+
// chatHistory kept as in-memory cache (for fast frontend UI access), also written to chatStore for persistence
|
|
93
|
+
this.chatHistory = [];
|
|
94
|
+
// Chat session ID (used for chatStore file storage)
|
|
95
|
+
this.chatSessionId = `boss-secretary-${this.id}`;
|
|
96
|
+
chatStore.createSession(this.chatSessionId, {
|
|
97
|
+
title: `${bossName} & Secretary`,
|
|
98
|
+
participants: [bossName, 'Secretary'],
|
|
99
|
+
type: 'boss-secretary',
|
|
100
|
+
});
|
|
101
|
+
// Department progress reports
|
|
102
|
+
this.progressReports = [];
|
|
103
|
+
// Mailbox: private messages from Agents to boss
|
|
104
|
+
this.mailbox = [];
|
|
105
|
+
// Pending recruitment plans for approval
|
|
106
|
+
this.pendingPlans = new Map();
|
|
107
|
+
|
|
108
|
+
// Message bus (inter-Agent communication)
|
|
109
|
+
this.messageBus = new MessageBus();
|
|
110
|
+
|
|
111
|
+
// Workspace manager
|
|
112
|
+
this.workspaceManager = new WorkspaceManager();
|
|
113
|
+
|
|
114
|
+
// Requirement manager
|
|
115
|
+
this.requirementManager = new RequirementManager();
|
|
116
|
+
|
|
117
|
+
// Team manager
|
|
118
|
+
this.teamManager = new TeamManager();
|
|
119
|
+
|
|
120
|
+
// Group chat loop engine
|
|
121
|
+
this.groupChatLoop = groupChatLoop;
|
|
122
|
+
|
|
123
|
+
// Configure provider for secretary
|
|
124
|
+
let secretaryProviderConfig;
|
|
125
|
+
if (secretaryConfig && secretaryConfig.providerId) {
|
|
126
|
+
const provider = this.providerRegistry.getById(secretaryConfig.providerId);
|
|
127
|
+
if (provider) {
|
|
128
|
+
this.providerRegistry.configure(secretaryConfig.providerId, secretaryConfig.apiKey || 'sk-configured');
|
|
129
|
+
secretaryProviderConfig = provider;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
// Fallback: use a placeholder provider reference (NOT enabled — user must configure a real one via onboarding)
|
|
133
|
+
if (!secretaryProviderConfig) {
|
|
134
|
+
secretaryProviderConfig = { id: 'none', name: '⚠️ Not Configured', enabled: false, category: 'general' };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Initialize personal secretary
|
|
138
|
+
this.secretary = new Secretary({
|
|
139
|
+
company: this,
|
|
140
|
+
providerConfig: secretaryProviderConfig,
|
|
141
|
+
secretaryName: secretaryConfig?.secretaryName,
|
|
142
|
+
secretaryAvatar: secretaryConfig?.secretaryAvatar,
|
|
143
|
+
secretaryGender: secretaryConfig?.secretaryGender || 'female',
|
|
144
|
+
secretaryAge: secretaryConfig?.secretaryAge || 18,
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
// If secretary uses a CLI provider, rebuild agent as CLIAgent
|
|
148
|
+
if (secretaryProviderConfig.isCLI && secretaryProviderConfig.cliBackendId) {
|
|
149
|
+
const fallback = this.providerRegistry.recommend('general');
|
|
150
|
+
this.secretary.agent = new CLIAgent({
|
|
151
|
+
cliBackend: secretaryProviderConfig.cliBackendId,
|
|
152
|
+
cliProvider: secretaryProviderConfig,
|
|
153
|
+
fallbackProvider: fallback, provider: fallback,
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
// If secretary uses a Web provider, rebuild agent as WebAgent
|
|
157
|
+
if (secretaryProviderConfig.isWeb) {
|
|
158
|
+
this.secretary.agent = new WebAgent({ provider: secretaryProviderConfig });
|
|
159
|
+
// Re-bind employeeId after agent replacement (for per-employee session isolation)
|
|
160
|
+
this.secretary.agent.setEmployeeId(this.secretary.id);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Initialize secretary's toolKit so she can use tools (shell, file ops, etc.)
|
|
164
|
+
const secretaryWorkspace = this.workspaceManager.createDepartmentWorkspace('secretary', 'secretary');
|
|
165
|
+
this.secretary.initToolKit(secretaryWorkspace, this.messageBus);
|
|
166
|
+
|
|
167
|
+
this._log('Company founded', `"${this.name}" founded by ${this.bossName}`);
|
|
168
|
+
this._log('Secretary ready', `Secretary ${this.secretary.name} using model ${secretaryProviderConfig.name}`);
|
|
169
|
+
|
|
170
|
+
// Initialize distilled subsystems
|
|
171
|
+
this._initSubsystems();
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Chat with secretary (task assignment or casual conversation)
|
|
176
|
+
* @param {string} message - Boss's message
|
|
177
|
+
* @returns {Promise<object>} Secretary's reply
|
|
178
|
+
*/
|
|
179
|
+
async chatWithSecretary(message) {
|
|
180
|
+
const bossMsg = {
|
|
181
|
+
role: 'boss',
|
|
182
|
+
content: message,
|
|
183
|
+
time: new Date(),
|
|
184
|
+
};
|
|
185
|
+
this.chatHistory.push(bossMsg);
|
|
186
|
+
// Persist to file storage
|
|
187
|
+
chatStore.appendMessage(this.chatSessionId, bossMsg);
|
|
188
|
+
|
|
189
|
+
const sec = this.secretary;
|
|
190
|
+
let reply;
|
|
191
|
+
|
|
192
|
+
// If secretary has CLI backend configured, chat also goes through CLI
|
|
193
|
+
if (sec.cliBackend) {
|
|
194
|
+
try {
|
|
195
|
+
const recentMessages = chatStore.getRecentMessages(this.chatSessionId, 10);
|
|
196
|
+
const chatContext = recentMessages.slice(-6).map(m =>
|
|
197
|
+
`${m.role === 'boss' ? 'Boss' : sec.name}: ${m.content}`
|
|
198
|
+
).join('\n');
|
|
199
|
+
|
|
200
|
+
const departments = [...this.departments.values()].map(d => ({
|
|
201
|
+
name: d.name, id: d.id, mission: d.mission, status: d.status,
|
|
202
|
+
memberCount: d.agents.size,
|
|
203
|
+
leader: d.getLeader()?.name || 'Unassigned',
|
|
204
|
+
}));
|
|
205
|
+
const deptContext = departments.length > 0
|
|
206
|
+
? departments.map(d => ` 🏢 ${d.name} [id:${d.id}] - Mission: ${d.mission} | ${d.memberCount} people | Leader: ${d.leader}`).join('\n')
|
|
207
|
+
: 'No departments yet.';
|
|
208
|
+
|
|
209
|
+
const cliPrompt = `You are "${sec.name}", the personal secretary of "${this.bossName}".
|
|
210
|
+
${sec.prompt ? `Your persona: ${sec.prompt}\n` : ''}
|
|
211
|
+
Current company "${this.name}" status:
|
|
212
|
+
- Departments: ${this.departments.size}
|
|
213
|
+
${deptContext}
|
|
214
|
+
|
|
215
|
+
Recent conversation:
|
|
216
|
+
${chatContext}
|
|
217
|
+
|
|
218
|
+
Boss's latest message: ${message}
|
|
219
|
+
|
|
220
|
+
You MUST reply with a JSON object (return JSON only, no other text):
|
|
221
|
+
{
|
|
222
|
+
"content": "Your natural language reply to the boss (warm, personal, with emoji)",
|
|
223
|
+
"action": null or one of:
|
|
224
|
+
- { "type": "secretary_handle", "taskDescription": "detailed task for yourself to execute" } — for simple tasks you can handle alone
|
|
225
|
+
- { "type": "task_assigned", "departmentId": "real dept id from above", "departmentName": "dept name", "taskTitle": "short title", "taskDescription": "detailed description" } — assign to existing department
|
|
226
|
+
- { "type": "create_department", "departmentName": "name", "mission": "mission", "members": [{ "templateId": "id", "name": "nickname", "isLeader": true/false, "reportsTo": null or 0 }] } — boss wants to create a new department (design team directly)
|
|
227
|
+
- { "type": "need_new_department", "suggestedMission": "task description" } — no existing dept can handle this
|
|
228
|
+
- { "type": "progress_report" } — boss wants progress
|
|
229
|
+
- null — casual chat, no action needed
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
Available job templates for team design (create_department): ${JSON.stringify(Object.values(JobTemplates).map(t => ({ id: t.id, title: t.title, category: t.category })))}
|
|
233
|
+
Enabled provider categories: ${[...new Set(this.providerRegistry.listEnabled().map(p => p.category))].join(', ')}
|
|
234
|
+
ONLY use templates whose category has an enabled provider above.
|
|
235
|
+
|
|
236
|
+
Rules:
|
|
237
|
+
- If boss wants to create a department → create_department (MUST include members array with 2-6 people, first must be project-leader with isLeader=true)
|
|
238
|
+
- If boss gives a task and an existing department matches → task_assigned (use the real departmentId!)
|
|
239
|
+
- If boss gives a simple task you can handle alone → secretary_handle
|
|
240
|
+
- If boss gives a task but no department matches → need_new_department
|
|
241
|
+
- Casual chat → null
|
|
242
|
+
- ALWAYS return valid JSON only, no markdown fences`;
|
|
243
|
+
|
|
244
|
+
const cliResult = await cliBackendRegistry.executeTask(
|
|
245
|
+
sec.cliBackend, sec,
|
|
246
|
+
{ title: `Secretary chat reply`, description: cliPrompt },
|
|
247
|
+
sec.toolKit?.workspaceDir || process.cwd(), {}, { timeout: 60000 }
|
|
248
|
+
);
|
|
249
|
+
const rawOutput = cliResult.output || cliResult.errorOutput || '...';
|
|
250
|
+
|
|
251
|
+
reply = this._parseSecretaryJSON(rawOutput, message);
|
|
252
|
+
} catch (cliErr) {
|
|
253
|
+
const hasLLM = sec.agent.provider && sec.agent.provider.enabled && sec.agent.provider.apiKey && !sec.agent.provider.apiKey.startsWith('cli');
|
|
254
|
+
if (hasLLM) {
|
|
255
|
+
console.warn(` ⚠️ [Secretary] CLI chat failed, falling back to LLM: ${cliErr.message || cliErr.error}`);
|
|
256
|
+
reply = await this.secretary.handleBossMessage(message, this);
|
|
257
|
+
} else {
|
|
258
|
+
// No available LLM provider, return CLI error message
|
|
259
|
+
console.error(` ❌ [Secretary] CLI chat failed, no LLM fallback available: ${cliErr.message || cliErr.error}`);
|
|
260
|
+
reply = {
|
|
261
|
+
content: `⚠️ CLI execution error: ${cliErr.message || 'Unknown error'}. Please check if CodeBuddy CLI is running properly.`,
|
|
262
|
+
action: null,
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
} else {
|
|
267
|
+
// Let secretary analyze whether it's task assignment or casual conversation
|
|
268
|
+
reply = await this.secretary.handleBossMessage(message, this);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const secretaryMsg = {
|
|
272
|
+
role: 'secretary',
|
|
273
|
+
content: reply.content,
|
|
274
|
+
action: reply.action || null,
|
|
275
|
+
time: new Date(),
|
|
276
|
+
};
|
|
277
|
+
this.chatHistory.push(secretaryMsg);
|
|
278
|
+
// Persist to file storage
|
|
279
|
+
chatStore.appendMessage(this.chatSessionId, secretaryMsg);
|
|
280
|
+
|
|
281
|
+
// Keep only the latest 50 messages in memory (for frontend cache)
|
|
282
|
+
if (this.chatHistory.length > 50) {
|
|
283
|
+
this.chatHistory = this.chatHistory.slice(-50);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
this._log('Secretary chat', `Boss: "${message.slice(0, 30)}..." → Secretary replied`);
|
|
287
|
+
return reply;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Parse secretary's returned JSON (shared by CLI and LLM)
|
|
292
|
+
* Extracts { content, action } structure from raw text
|
|
293
|
+
*/
|
|
294
|
+
_parseSecretaryJSON(rawOutput, originalMessage) {
|
|
295
|
+
try {
|
|
296
|
+
const parsed = robustJSONParse(rawOutput);
|
|
297
|
+
const result = {
|
|
298
|
+
content: parsed.content || rawOutput,
|
|
299
|
+
action: parsed.action || null,
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
// Validate task_assigned departmentId
|
|
303
|
+
if (result.action?.type === 'task_assigned' && result.action.departmentId) {
|
|
304
|
+
const deptById = this.departments.get(result.action.departmentId);
|
|
305
|
+
if (!deptById) {
|
|
306
|
+
// departmentId invalid, try matching by name
|
|
307
|
+
const deptIdValue = result.action.departmentId;
|
|
308
|
+
const deptNameValue = result.action.departmentName || deptIdValue;
|
|
309
|
+
let foundDept = null;
|
|
310
|
+
for (const dept of this.departments.values()) {
|
|
311
|
+
if (dept.name === deptIdValue || dept.name === deptNameValue ||
|
|
312
|
+
dept.name.includes(deptIdValue) || deptIdValue.includes(dept.name)) {
|
|
313
|
+
foundDept = dept;
|
|
314
|
+
break;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
if (foundDept) {
|
|
318
|
+
console.log(`🔧 [CLI] Fixed departmentId: "${deptIdValue}" → "${foundDept.id}" (${foundDept.name})`);
|
|
319
|
+
result.action.departmentId = foundDept.id;
|
|
320
|
+
result.action.departmentName = foundDept.name;
|
|
321
|
+
} else {
|
|
322
|
+
console.warn(`⚠️ [CLI] departmentId "${deptIdValue}" doesn't match any department, clearing action`);
|
|
323
|
+
result.action = null;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
console.log(`🤖 [Secretary-CLI] action type: ${result.action?.type || 'null'}`);
|
|
329
|
+
return result;
|
|
330
|
+
} catch (parseError) {
|
|
331
|
+
console.warn('⚠️ [Secretary-CLI] JSON parse failed, using raw output:', parseError.message);
|
|
332
|
+
// JSON parse failed, try to extract content field
|
|
333
|
+
let displayContent = rawOutput;
|
|
334
|
+
const contentFieldMatch = rawOutput.match(/"content"\s*:\s*"((?:[^"\\]|\\.)*)"/);
|
|
335
|
+
if (contentFieldMatch) {
|
|
336
|
+
try {
|
|
337
|
+
displayContent = JSON.parse('"' + contentFieldMatch[1] + '"');
|
|
338
|
+
} catch {
|
|
339
|
+
displayContent = contentFieldMatch[1].replace(/\\n/g, '\n').replace(/\\"/g, '"');
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Try to extract action from raw output
|
|
344
|
+
let action = null;
|
|
345
|
+
const actionTypeMatch = rawOutput.match(/"type"\s*:\s*"(task_assigned|need_new_department|create_department|progress_report|secretary_handle)"/);
|
|
346
|
+
if (actionTypeMatch) {
|
|
347
|
+
const actionType = actionTypeMatch[1];
|
|
348
|
+
if (actionType === 'task_assigned') {
|
|
349
|
+
const deptNameMatch = rawOutput.match(/"departmentName"\s*:\s*"([^"]+)"/);
|
|
350
|
+
if (deptNameMatch) {
|
|
351
|
+
for (const dept of this.departments.values()) {
|
|
352
|
+
if (dept.name === deptNameMatch[1] || dept.name.includes(deptNameMatch[1]) || deptNameMatch[1].includes(dept.name)) {
|
|
353
|
+
const titleMatch = rawOutput.match(/"taskTitle"\s*:\s*"([^"]+)"/);
|
|
354
|
+
const descMatch = rawOutput.match(/"taskDescription"\s*:\s*"((?:[^"\\]|\\.)*)"/);
|
|
355
|
+
action = {
|
|
356
|
+
type: 'task_assigned',
|
|
357
|
+
departmentId: dept.id,
|
|
358
|
+
departmentName: dept.name,
|
|
359
|
+
taskTitle: titleMatch ? titleMatch[1] : originalMessage.slice(0, 50),
|
|
360
|
+
taskDescription: descMatch ? descMatch[1].replace(/\\n/g, '\n') : originalMessage,
|
|
361
|
+
};
|
|
362
|
+
break;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
} else if (actionType === 'create_department') {
|
|
367
|
+
const deptNameMatch = rawOutput.match(/"departmentName"\s*:\s*"([^"]+)"/);
|
|
368
|
+
const missionMatch = rawOutput.match(/"mission"\s*:\s*"((?:[^"\\]|\\.)*)"/);
|
|
369
|
+
action = { type: 'create_department', departmentName: deptNameMatch?.[1] || '', mission: missionMatch?.[1]?.replace(/\\n/g, '\n') || originalMessage };
|
|
370
|
+
} else if (actionType === 'need_new_department') {
|
|
371
|
+
const missionMatch = rawOutput.match(/"suggestedMission"\s*:\s*"((?:[^"\\]|\\.)*)"/);
|
|
372
|
+
action = { type: 'need_new_department', suggestedMission: missionMatch?.[1]?.replace(/\\n/g, '\n') || originalMessage };
|
|
373
|
+
} else if (actionType === 'secretary_handle') {
|
|
374
|
+
const descMatch = rawOutput.match(/"taskDescription"\s*:\s*"((?:[^"\\]|\\.)*)"/);
|
|
375
|
+
action = { type: 'secretary_handle', taskDescription: descMatch?.[1]?.replace(/\\n/g, '\n') || originalMessage };
|
|
376
|
+
} else if (actionType === 'progress_report') {
|
|
377
|
+
action = { type: 'progress_report' };
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
return { content: displayContent, action };
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Update boss profile (avatar)
|
|
387
|
+
*/
|
|
388
|
+
updateBossProfile(settings) {
|
|
389
|
+
if (settings.avatar) {
|
|
390
|
+
this.bossAvatar = settings.avatar;
|
|
391
|
+
}
|
|
392
|
+
this._log('Boss profile updated', `Avatar updated`);
|
|
393
|
+
return { bossAvatar: this.bossAvatar };
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* Chat with a specific agent (boss <-> agent 1-on-1)
|
|
398
|
+
* @param {string} agentId - Agent ID
|
|
399
|
+
* @param {string} message - Boss's message
|
|
400
|
+
* @returns {Promise<object>} Agent's reply
|
|
401
|
+
*/
|
|
402
|
+
async chatWithAgent(agentId, message) {
|
|
403
|
+
// Find the agent
|
|
404
|
+
let targetAgent = null;
|
|
405
|
+
let targetDept = null;
|
|
406
|
+
for (const dept of this.departments.values()) {
|
|
407
|
+
const agent = dept.agents.get(agentId);
|
|
408
|
+
if (agent) {
|
|
409
|
+
targetAgent = agent;
|
|
410
|
+
targetDept = dept;
|
|
411
|
+
break;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
if (!targetAgent) throw new Error(`Employee not found: ${agentId}`);
|
|
415
|
+
|
|
416
|
+
// Create or get chat session
|
|
417
|
+
const sessionId = `boss-agent-${agentId}`;
|
|
418
|
+
chatStore.createSession(sessionId, {
|
|
419
|
+
title: `${this.bossName} & ${targetAgent.name}`,
|
|
420
|
+
participants: [this.bossName, targetAgent.name],
|
|
421
|
+
type: 'boss-agent',
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
// Append boss message
|
|
425
|
+
const bossMsg = { role: 'boss', content: message, time: new Date() };
|
|
426
|
+
chatStore.appendMessage(sessionId, bossMsg);
|
|
427
|
+
|
|
428
|
+
// Get recent chat history for context
|
|
429
|
+
const recentMessages = chatStore.getRecentMessages(sessionId, 20);
|
|
430
|
+
|
|
431
|
+
// Build memory context from the new Memory system
|
|
432
|
+
const bossChatGroupId = `boss-chat-${agentId}`;
|
|
433
|
+
const memoryContext = targetAgent.memory.buildMemoryContext(bossChatGroupId);
|
|
434
|
+
|
|
435
|
+
// Build messages for LLM — now with structured memory + JSON output
|
|
436
|
+
const systemMessage = targetAgent._buildSystemMessage()
|
|
437
|
+
+ `\n\n## Current Conversation\nYou are having a private 1-on-1 conversation with your boss "${this.bossName}".`
|
|
438
|
+
+ ` You work in the "${targetDept.name}" department.`
|
|
439
|
+
+ ` Respond naturally based on your personality and role. Be helpful but stay in character.`
|
|
440
|
+
+ memoryContext
|
|
441
|
+
+ `\n\n## Output Format\nYou MUST return a JSON object (JSON only, nothing else):\n{\n "content": "Your natural language reply",\n "memorySummary": "A concise summary of older messages in this conversation — key facts, decisions, topics discussed. null if conversation just started.",\n "memoryOps": [\n { "op": "add", "type": "long_term", "content": "Important fact about the boss or decision made", "category": "fact", "importance": 8 },\n { "op": "add", "type": "short_term", "content": "Current topic context", "category": "context", "importance": 5, "ttl": 3600 },\n { "op": "delete", "id": "mem_id_to_forget" }\n ],\n "relationshipOps": [\n { "employeeId": "boss", "name": "Boss", "impression": "Demanding but fair, values results", "affinity": 65 }\n ]\n}\n\n## Memory Management\n- memorySummary: Summarize older conversation messages to compress context. Keep key info, skip chitchat. null if no old messages.\n- memoryOps: Manage your memory — add important facts/preferences about the boss as long_term, add current topic as short_term. [] if nothing to remember.\n- category: preference | fact | instruction | task | context | relationship | experience\n- importance: 1-10 (higher = more important)\n\n## Relationship Impressions\n- relationshipOps: Update your impression of the boss based on this conversation. Max 30 chars per impression, affinity 1-100 (50=neutral).\n- affinity should change gradually (+/- 5~15 per interaction). Start from 50 if first meeting.\n- Only update when something noteworthy happened. [] if nothing to update.`;
|
|
442
|
+
|
|
443
|
+
const messages = [
|
|
444
|
+
{ role: 'system', content: systemMessage },
|
|
445
|
+
];
|
|
446
|
+
|
|
447
|
+
// Add recent history as context
|
|
448
|
+
for (const msg of recentMessages.slice(0, -1)) { // exclude the one we just added
|
|
449
|
+
messages.push({
|
|
450
|
+
role: msg.role === 'boss' ? 'user' : 'assistant',
|
|
451
|
+
content: msg.content,
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// Add current message
|
|
456
|
+
messages.push({ role: 'user', content: message });
|
|
457
|
+
|
|
458
|
+
// If this is a CLI agent, execute chat via CLI backend
|
|
459
|
+
let replyContent;
|
|
460
|
+
const displayInfo = targetAgent.getDisplayInfo();
|
|
461
|
+
const chatEngine = displayInfo.type === 'cli'
|
|
462
|
+
? { engine: 'cli', cliName: displayInfo.name }
|
|
463
|
+
: displayInfo.type === 'web'
|
|
464
|
+
? { engine: 'web', webName: displayInfo.name }
|
|
465
|
+
: { engine: 'llm', llmName: displayInfo.name };
|
|
466
|
+
|
|
467
|
+
if (targetAgent.agentType === 'cli' && targetAgent.isAvailable()) {
|
|
468
|
+
// CLI agent chat goes through CLI backend
|
|
469
|
+
try {
|
|
470
|
+
const chatContext = recentMessages.slice(-6).map(m =>
|
|
471
|
+
`${m.role === 'boss' ? 'Boss' : targetAgent.name}: ${m.content}`
|
|
472
|
+
).join('\n');
|
|
473
|
+
|
|
474
|
+
const cliResult = await cliBackendRegistry.executeTask(
|
|
475
|
+
targetAgent.cliBackend,
|
|
476
|
+
targetAgent,
|
|
477
|
+
{
|
|
478
|
+
title: `Chat reply`,
|
|
479
|
+
description: `You are having a 1-on-1 conversation with your boss "${this.bossName}". Reply naturally and helpfully based on your personality and role. Keep your reply concise (2-6 sentences). Do NOT use any tools or execute any code — just reply conversationally.\n\nRecent conversation:\n${chatContext}\n\nBoss: ${message}\n\nReply as ${targetAgent.name}:`,
|
|
480
|
+
},
|
|
481
|
+
targetAgent.toolKit?.workspaceDir || process.cwd(),
|
|
482
|
+
{},
|
|
483
|
+
{ timeout: 60000 }
|
|
484
|
+
);
|
|
485
|
+
replyContent = cliResult.output || cliResult.errorOutput || '...';
|
|
486
|
+
} catch (cliErr) {
|
|
487
|
+
// On CLI failure, attempt fallback to LLM
|
|
488
|
+
if (targetAgent.canChat()) {
|
|
489
|
+
console.warn(` ⚠️ [${targetAgent.name}] CLI chat failed, falling back to LLM: ${cliErr.message || cliErr.error}`);
|
|
490
|
+
try {
|
|
491
|
+
const response = await targetAgent.chat(messages, { temperature: 0.8, maxTokens: 2048 });
|
|
492
|
+
replyContent = response.content;
|
|
493
|
+
} catch (err) {
|
|
494
|
+
replyContent = `(Sorry boss, my brain froze: ${err.message})`;
|
|
495
|
+
}
|
|
496
|
+
} else {
|
|
497
|
+
console.error(` ❌ [${targetAgent.name}] CLI chat failed, no LLM fallback: ${cliErr.message || cliErr.error}`);
|
|
498
|
+
replyContent = `⚠️ CLI execution error: ${cliErr.message || 'Unknown error'}. Please check if CLI is running properly.`;
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
} else if (targetAgent.canChat()) {
|
|
502
|
+
// Regular LLM agent or CLI with fallback
|
|
503
|
+
try {
|
|
504
|
+
const response = await targetAgent.chat(messages, { temperature: 0.8, maxTokens: 2048 });
|
|
505
|
+
replyContent = response.content;
|
|
506
|
+
} catch (err) {
|
|
507
|
+
replyContent = `(Sorry boss, my brain froze: ${err.message})`;
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// Process structured memory from AI response (new Memory system)
|
|
512
|
+
try {
|
|
513
|
+
const { robustJSONParse } = await import('../utils/json-parse.js');
|
|
514
|
+
const parsed = robustJSONParse(replyContent);
|
|
515
|
+
if (parsed && parsed.content) {
|
|
516
|
+
// Extract actual reply content from JSON
|
|
517
|
+
replyContent = parsed.content;
|
|
518
|
+
|
|
519
|
+
// Process rolling history summary
|
|
520
|
+
if (parsed.memorySummary) {
|
|
521
|
+
targetAgent.memory.updateHistorySummary(bossChatGroupId, parsed.memorySummary);
|
|
522
|
+
console.log(` 📜 [${targetAgent.name}] Boss-chat history summary updated`);
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
// Process memory operations
|
|
526
|
+
if (parsed.memoryOps && Array.isArray(parsed.memoryOps)) {
|
|
527
|
+
const result = targetAgent.memory.processMemoryOps(parsed.memoryOps);
|
|
528
|
+
if (result.added + result.updated + result.deleted > 0) {
|
|
529
|
+
console.log(` 🧠 [${targetAgent.name}] Boss-chat memory: +${result.added} ~${result.updated} -${result.deleted}`);
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
// Process relationship impressions
|
|
534
|
+
if (parsed.relationshipOps && Array.isArray(parsed.relationshipOps)) {
|
|
535
|
+
const relResult = targetAgent.memory.processRelationshipOps(parsed.relationshipOps);
|
|
536
|
+
if (relResult.updated > 0) {
|
|
537
|
+
console.log(` 👥 [${targetAgent.name}] Boss-chat relationship updates: ${relResult.updated}`);
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
} catch (e) {
|
|
542
|
+
// JSON parse failed — replyContent is plain text, that's fine
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
// Append agent reply
|
|
546
|
+
const agentMsg = { role: 'agent', content: replyContent, time: new Date() };
|
|
547
|
+
chatStore.appendMessage(sessionId, agentMsg);
|
|
548
|
+
|
|
549
|
+
|
|
550
|
+
|
|
551
|
+
this.save();
|
|
552
|
+
|
|
553
|
+
return {
|
|
554
|
+
agentId: targetAgent.id,
|
|
555
|
+
agentName: targetAgent.name,
|
|
556
|
+
reply: replyContent,
|
|
557
|
+
time: new Date(),
|
|
558
|
+
chatEngine,
|
|
559
|
+
};
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
/**
|
|
563
|
+
* Get chat history with a specific agent
|
|
564
|
+
* @param {string} agentId - Agent ID
|
|
565
|
+
* @param {number} limit - Max messages to return
|
|
566
|
+
* @returns {Array} Chat messages
|
|
567
|
+
*/
|
|
568
|
+
getAgentChatHistory(agentId, limit = 30) {
|
|
569
|
+
const sessionId = `boss-agent-${agentId}`;
|
|
570
|
+
return chatStore.getRecentMessages(sessionId, limit);
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
/**
|
|
574
|
+
* Mark chat with agent as read
|
|
575
|
+
*/
|
|
576
|
+
markAgentChatRead(agentId) {
|
|
577
|
+
const sessionId = `boss-agent-${agentId}`;
|
|
578
|
+
chatStore.markSessionRead(sessionId);
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
/**
|
|
582
|
+
* Get summary info for all boss-agent private chat sessions
|
|
583
|
+
* Used to display private chat session list in Mailbox
|
|
584
|
+
*/
|
|
585
|
+
_getAgentChatSessions() {
|
|
586
|
+
const sessions = chatStore.listSessions();
|
|
587
|
+
const agentSessions = sessions.filter(s => s.type === 'boss-agent');
|
|
588
|
+
|
|
589
|
+
return agentSessions.map(session => {
|
|
590
|
+
// Extract agentId from sessionId: "boss-agent-{agentId}"
|
|
591
|
+
const agentId = session.sessionId.replace('boss-agent-', '');
|
|
592
|
+
|
|
593
|
+
// Find corresponding agent info
|
|
594
|
+
let agent = null;
|
|
595
|
+
let deptName = null;
|
|
596
|
+
for (const dept of this.departments.values()) {
|
|
597
|
+
const a = dept.agents.get(agentId);
|
|
598
|
+
if (a) {
|
|
599
|
+
agent = a;
|
|
600
|
+
deptName = dept.name;
|
|
601
|
+
break;
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
// Get latest message as preview
|
|
606
|
+
const recentMessages = chatStore.getRecentMessages(session.sessionId, 1);
|
|
607
|
+
const lastMsg = recentMessages.length > 0 ? recentMessages[recentMessages.length - 1] : null;
|
|
608
|
+
|
|
609
|
+
// Get read timestamp to determine if there are unread messages
|
|
610
|
+
const meta = chatStore.getSessionMeta(session.sessionId);
|
|
611
|
+
const bossLastReadAt = meta?.bossLastReadAt || null;
|
|
612
|
+
const lastTime = lastMsg?.time || session.lastActiveAt || session.createdAt;
|
|
613
|
+
// If never marked as read, or latest message time is after read time, it is unread
|
|
614
|
+
const unread = !bossLastReadAt || (lastTime && new Date(lastTime) > new Date(bossLastReadAt));
|
|
615
|
+
|
|
616
|
+
return {
|
|
617
|
+
sessionId: session.sessionId,
|
|
618
|
+
agentId,
|
|
619
|
+
agentName: agent?.name || session.participants?.[1] || 'Unknown',
|
|
620
|
+
agentAvatar: agent?.avatar || null,
|
|
621
|
+
agentRole: agent?.role || null,
|
|
622
|
+
agentSignature: agent?.signature || null,
|
|
623
|
+
departmentName: deptName,
|
|
624
|
+
lastMessage: lastMsg?.content?.slice(0, 50) || null,
|
|
625
|
+
lastMessageRole: lastMsg?.role || null,
|
|
626
|
+
lastTime,
|
|
627
|
+
totalMessages: session.totalMessages || 0,
|
|
628
|
+
unread,
|
|
629
|
+
};
|
|
630
|
+
}).filter(s => s.totalMessages > 0 && s.agentName !== 'Unknown' && s.agentAvatar !== null) // Only return sessions with messages, filter out dismissed agents (cannot find agent)
|
|
631
|
+
.sort((a, b) => new Date(b.lastTime) - new Date(a.lastTime)); // Sort by time descending
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
/**
|
|
635
|
+
* Get all agent-to-agent chat sessions for a given agent
|
|
636
|
+
* Used to "view chat records between this agent and others"
|
|
637
|
+
* @param {string} agentId - Target agent ID
|
|
638
|
+
* @returns {Array} Chat session list
|
|
639
|
+
*/
|
|
640
|
+
getAgentConversations(agentId) {
|
|
641
|
+
const sessions = chatStore.listSessions();
|
|
642
|
+
// Find all agent-agent sessions that include this agent
|
|
643
|
+
const agentSessions = sessions.filter(s => {
|
|
644
|
+
if (s.type !== 'agent-agent') return false;
|
|
645
|
+
// sessionId format: agent-agent-{id1}-{id2}
|
|
646
|
+
return s.sessionId.includes(agentId);
|
|
647
|
+
});
|
|
648
|
+
|
|
649
|
+
// Also include boss-agent chats
|
|
650
|
+
const bossSessions = sessions.filter(s =>
|
|
651
|
+
s.type === 'boss-agent' && s.sessionId === `boss-agent-${agentId}`
|
|
652
|
+
);
|
|
653
|
+
|
|
654
|
+
const conversations = [];
|
|
655
|
+
|
|
656
|
+
for (const session of agentSessions) {
|
|
657
|
+
// Use participants to find the other party ID (more reliable than parsing sessionId)
|
|
658
|
+
const participants = session.participants || [];
|
|
659
|
+
const peerId = participants.find(p => p !== agentId) || null;
|
|
660
|
+
if (!peerId) continue;
|
|
661
|
+
|
|
662
|
+
// Find the other agent info
|
|
663
|
+
let peerAgent = null;
|
|
664
|
+
let peerDeptName = null;
|
|
665
|
+
for (const dept of this.departments.values()) {
|
|
666
|
+
const a = dept.agents.get(peerId);
|
|
667
|
+
if (a) {
|
|
668
|
+
peerAgent = a;
|
|
669
|
+
peerDeptName = dept.name;
|
|
670
|
+
break;
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
const recentMessages = chatStore.getRecentMessages(session.sessionId, 1);
|
|
675
|
+
const lastMsg = recentMessages.length > 0 ? recentMessages[recentMessages.length - 1] : null;
|
|
676
|
+
|
|
677
|
+
conversations.push({
|
|
678
|
+
sessionId: session.sessionId,
|
|
679
|
+
type: 'agent-agent',
|
|
680
|
+
peerId,
|
|
681
|
+
peerName: peerAgent?.name || session.participants?.find(p => p !== agentId) || 'Unknown',
|
|
682
|
+
peerAvatar: peerAgent?.avatar || null,
|
|
683
|
+
peerRole: peerAgent?.role || null,
|
|
684
|
+
peerDepartment: peerDeptName,
|
|
685
|
+
lastMessage: lastMsg?.content?.slice(0, 60) || null,
|
|
686
|
+
lastTime: lastMsg?.time || session.lastActiveAt || session.createdAt,
|
|
687
|
+
totalMessages: session.totalMessages || 0,
|
|
688
|
+
});
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
// Boss chat
|
|
692
|
+
for (const session of bossSessions) {
|
|
693
|
+
const recentMessages = chatStore.getRecentMessages(session.sessionId, 1);
|
|
694
|
+
const lastMsg = recentMessages.length > 0 ? recentMessages[recentMessages.length - 1] : null;
|
|
695
|
+
|
|
696
|
+
conversations.push({
|
|
697
|
+
sessionId: session.sessionId,
|
|
698
|
+
type: 'boss-agent',
|
|
699
|
+
peerId: 'boss',
|
|
700
|
+
peerName: this.bossName || 'Boss',
|
|
701
|
+
peerAvatar: this.bossAvatar || null,
|
|
702
|
+
peerRole: 'Boss',
|
|
703
|
+
peerDepartment: null,
|
|
704
|
+
lastMessage: lastMsg?.content?.slice(0, 60) || null,
|
|
705
|
+
lastTime: lastMsg?.time || session.lastActiveAt || session.createdAt,
|
|
706
|
+
totalMessages: session.totalMessages || 0,
|
|
707
|
+
});
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
// Sort by time descending
|
|
711
|
+
conversations.sort((a, b) => {
|
|
712
|
+
if (!a.lastTime) return 1;
|
|
713
|
+
if (!b.lastTime) return -1;
|
|
714
|
+
return new Date(b.lastTime) - new Date(a.lastTime);
|
|
715
|
+
});
|
|
716
|
+
|
|
717
|
+
return conversations;
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
/**
|
|
721
|
+
* Get chat messages for an agent-agent session
|
|
722
|
+
* @param {string} sessionId - Session ID
|
|
723
|
+
* @param {number} limit - Max message count
|
|
724
|
+
* @returns {Array} Message list
|
|
725
|
+
*/
|
|
726
|
+
getAgentAgentChatHistory(sessionId, limit = 50) {
|
|
727
|
+
const messages = chatStore.getRecentMessages(sessionId, limit);
|
|
728
|
+
// Also return session participants info, for frontend to determine message direction
|
|
729
|
+
const meta = chatStore.getSessionMeta(sessionId);
|
|
730
|
+
return {
|
|
731
|
+
messages,
|
|
732
|
+
participants: meta?.participants || [],
|
|
733
|
+
};
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
/**
|
|
737
|
+
* Update secretary settings (name, avatar, prompt, etc.)
|
|
738
|
+
*/
|
|
739
|
+
updateSecretarySettings(settings) {
|
|
740
|
+
const sec = this.secretary;
|
|
741
|
+
if (settings.name) sec.name = settings.name;
|
|
742
|
+
if (settings.avatar) sec.avatar = settings.avatar;
|
|
743
|
+
if (settings.avatarParams) sec.avatarParams = settings.avatarParams;
|
|
744
|
+
if (settings.gender) sec.gender = settings.gender;
|
|
745
|
+
if (settings.age != null) sec.age = settings.age;
|
|
746
|
+
if (settings.prompt) sec.prompt = settings.prompt;
|
|
747
|
+
if (settings.signature) sec.signature = settings.signature;
|
|
748
|
+
// Switch provider
|
|
749
|
+
if (settings.providerId) {
|
|
750
|
+
const newProvider = this.providerRegistry.getById(settings.providerId);
|
|
751
|
+
if (!newProvider) throw new Error(`Provider not found: ${settings.providerId}`);
|
|
752
|
+
if (!newProvider.enabled) throw new Error(`Provider ${newProvider.name} is not enabled, please configure API Key first`);
|
|
753
|
+
|
|
754
|
+
// Determine target agent type
|
|
755
|
+
const targetType = newProvider.isCLI ? 'cli' : newProvider.isWeb ? 'web' : 'llm';
|
|
756
|
+
const needsTypeSwitch = sec.agentType !== targetType;
|
|
757
|
+
|
|
758
|
+
if (needsTypeSwitch) {
|
|
759
|
+
// Agent type mismatch — rebuild the communication agent only
|
|
760
|
+
if (newProvider.isCLI && newProvider.cliBackendId) {
|
|
761
|
+
const fallback = this.providerRegistry.recommend('general');
|
|
762
|
+
sec.agent = new CLIAgent({
|
|
763
|
+
cliBackend: newProvider.cliBackendId,
|
|
764
|
+
cliProvider: newProvider, fallbackProvider: fallback, provider: fallback,
|
|
765
|
+
});
|
|
766
|
+
this._log('Secretary settings', `Secretary agent rebuilt as CLIAgent: ${newProvider.name} (${newProvider.cliBackendId})`);
|
|
767
|
+
} else if (newProvider.isWeb) {
|
|
768
|
+
sec.agent = new WebAgent({ provider: newProvider });
|
|
769
|
+
sec.agent.setEmployeeId(sec.id);
|
|
770
|
+
// Reset session so next chat reinitializes with the new provider
|
|
771
|
+
sec._sessionAwake = false;
|
|
772
|
+
this._log('Secretary settings', `Secretary agent rebuilt as WebAgent: ${newProvider.name}`);
|
|
773
|
+
} else {
|
|
774
|
+
sec.agent = new LLMAgent({ provider: newProvider });
|
|
775
|
+
this._log('Secretary settings', `Secretary agent rebuilt as LLMAgent: ${newProvider.name}`);
|
|
776
|
+
}
|
|
777
|
+
} else {
|
|
778
|
+
sec.switchProvider(newProvider);
|
|
779
|
+
}
|
|
780
|
+
// Sync HR assistant's provider
|
|
781
|
+
sec.hrAssistant.employee.switchProvider(newProvider);
|
|
782
|
+
this._log('Secretary settings', `Secretary provider switched to: ${newProvider.name}`);
|
|
783
|
+
}
|
|
784
|
+
this._log('Secretary settings', `Updated secretary settings: ${Object.keys(settings).join(', ')}`);
|
|
785
|
+
this.save();
|
|
786
|
+
const displayInfo = sec.getProviderDisplayInfo();
|
|
787
|
+
return {
|
|
788
|
+
name: sec.name,
|
|
789
|
+
avatar: sec.avatar,
|
|
790
|
+
gender: sec.gender,
|
|
791
|
+
age: sec.age,
|
|
792
|
+
prompt: sec.prompt,
|
|
793
|
+
signature: sec.signature,
|
|
794
|
+
provider: displayInfo.name,
|
|
795
|
+
providerId: displayInfo.id,
|
|
796
|
+
};
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
/**
|
|
800
|
+
* Initialize distilled subsystems (hooks, cron, plugins, sessions)
|
|
801
|
+
*/
|
|
802
|
+
_initSubsystems() {
|
|
803
|
+
// 1. Configure cron executor to run tasks via company
|
|
804
|
+
cronScheduler.executor = async (agentId, taskPrompt, jobId) => {
|
|
805
|
+
// Find the agent and their department
|
|
806
|
+
for (const dept of this.departments.values()) {
|
|
807
|
+
const agent = dept.agents.get(agentId);
|
|
808
|
+
if (agent) {
|
|
809
|
+
const result = await agent.executeTask({
|
|
810
|
+
title: `Scheduled: ${taskPrompt.slice(0, 40)}`,
|
|
811
|
+
description: taskPrompt,
|
|
812
|
+
context: `This is an automated scheduled task (job: ${jobId})`,
|
|
813
|
+
});
|
|
814
|
+
return result.output;
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
throw new Error(`Agent ${agentId} not found for cron job`);
|
|
818
|
+
};
|
|
819
|
+
|
|
820
|
+
// 2. Register cron callbacks for hooks integration
|
|
821
|
+
cronScheduler.onJobRun = (job) => {
|
|
822
|
+
hookRegistry.trigger(HookEvent.TASK_ASSIGNED, {
|
|
823
|
+
source: 'cron', jobId: job.id, jobName: job.name, agentId: job.agentId,
|
|
824
|
+
});
|
|
825
|
+
};
|
|
826
|
+
cronScheduler.onJobComplete = (job, result) => {
|
|
827
|
+
hookRegistry.trigger(HookEvent.TASK_COMPLETED, {
|
|
828
|
+
source: 'cron', jobId: job.id, jobName: job.name, agentId: job.agentId,
|
|
829
|
+
});
|
|
830
|
+
};
|
|
831
|
+
cronScheduler.onJobError = (job, error) => {
|
|
832
|
+
hookRegistry.trigger(HookEvent.TASK_FAILED, {
|
|
833
|
+
source: 'cron', jobId: job.id, jobName: job.name, error: error.message,
|
|
834
|
+
});
|
|
835
|
+
auditLogger.log({
|
|
836
|
+
category: AuditCategory.AGENT_ACTION, level: AuditLevel.WARN,
|
|
837
|
+
agentId: job.agentId, action: `Cron job failed: ${job.name}`,
|
|
838
|
+
details: { error: error.message, jobId: job.id },
|
|
839
|
+
});
|
|
840
|
+
};
|
|
841
|
+
|
|
842
|
+
// 3. Enable built-in plugins by default
|
|
843
|
+
for (const plugin of pluginRegistry.list()) {
|
|
844
|
+
if (plugin.state === 'installed') {
|
|
845
|
+
try { pluginRegistry.enable(plugin.id); } catch {}
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
// 4. Start cron scheduler
|
|
850
|
+
cronScheduler.start();
|
|
851
|
+
|
|
852
|
+
// 5. Start session pruning
|
|
853
|
+
sessionManager.startPruning();
|
|
854
|
+
|
|
855
|
+
// 6. Start group chat loop engine
|
|
856
|
+
groupChatLoop.start(this);
|
|
857
|
+
// Start group chat loop for all existing agents
|
|
858
|
+
for (const dept of this.departments.values()) {
|
|
859
|
+
for (const agent of dept.getMembers()) {
|
|
860
|
+
groupChatLoop.startAgentLoop(agent);
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
// 7. Fire system startup hook
|
|
865
|
+
hookRegistry.trigger(HookEvent.SYSTEM_STARTUP, {
|
|
866
|
+
companyName: this.name, bossName: this.bossName,
|
|
867
|
+
});
|
|
868
|
+
|
|
869
|
+
console.log('⚡ Distilled subsystems initialized (hooks, cron, plugins, sessions)');
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
_log(action, detail) {
|
|
873
|
+
this.logs.push({ time: new Date(), action, detail });
|
|
874
|
+
// Auto-persist on every state change
|
|
875
|
+
debouncedSave(this);
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
/**
|
|
879
|
+
* Manually trigger persistence (call after important operations)
|
|
880
|
+
*/
|
|
881
|
+
save() {
|
|
882
|
+
debouncedSave(this, 500);
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
/**
|
|
886
|
+
* Step 1: Generate recruitment plan (don't execute, wait for boss approval)
|
|
887
|
+
*/
|
|
888
|
+
async planDepartment(name, mission) {
|
|
889
|
+
// Use a temporary Department instance for team design analysis
|
|
890
|
+
const tempDept = new Department({ name, mission, company: this.id });
|
|
891
|
+
const teamPlan = await tempDept.designTeam(mission, this.secretary, this.providerRegistry);
|
|
892
|
+
teamPlan.departmentName = name;
|
|
893
|
+
|
|
894
|
+
const planId = uuidv4();
|
|
895
|
+
this.pendingPlans.set(planId, { teamPlan, name, mission });
|
|
896
|
+
|
|
897
|
+
this._log('Recruitment plan', `Secretary planned a ${teamPlan.members.length}-person team for "${name}", pending boss approval`);
|
|
898
|
+
|
|
899
|
+
return {
|
|
900
|
+
planId,
|
|
901
|
+
departmentName: name,
|
|
902
|
+
mission,
|
|
903
|
+
reasoning: teamPlan.reasoning || null,
|
|
904
|
+
members: teamPlan.members.map(m => {
|
|
905
|
+
// Find job template for this position, get category and requiredCapabilities
|
|
906
|
+
const template = this.hr.getTemplate(m.templateId);
|
|
907
|
+
let providerName = null;
|
|
908
|
+
let providerModel = null;
|
|
909
|
+
if (template) {
|
|
910
|
+
const recommended = this.providerRegistry.recommend(
|
|
911
|
+
template.category,
|
|
912
|
+
template.requiredCapabilities
|
|
913
|
+
);
|
|
914
|
+
if (recommended) {
|
|
915
|
+
providerName = recommended.name;
|
|
916
|
+
providerModel = recommended.model;
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
return {
|
|
920
|
+
templateId: m.templateId,
|
|
921
|
+
title: m.templateTitle,
|
|
922
|
+
name: m.name,
|
|
923
|
+
isLeader: m.isLeader,
|
|
924
|
+
reportsTo: m.reportsTo !== null ? teamPlan.members[m.reportsTo]?.name : null,
|
|
925
|
+
reason: m.reason || null,
|
|
926
|
+
providerName, // Recommended provider name (model name)
|
|
927
|
+
providerModel, // Recommended provider model
|
|
928
|
+
};
|
|
929
|
+
}),
|
|
930
|
+
collaborationRules: teamPlan.collaborationRules,
|
|
931
|
+
};
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
/**
|
|
935
|
+
* Step 2: Confirm recruitment plan, execute hiring
|
|
936
|
+
*/
|
|
937
|
+
async confirmPlan(planId) {
|
|
938
|
+
const plan = this.pendingPlans.get(planId);
|
|
939
|
+
if (!plan) throw new Error('Recruitment plan not found or expired');
|
|
940
|
+
|
|
941
|
+
this.pendingPlans.delete(planId);
|
|
942
|
+
|
|
943
|
+
const { teamPlan, name, mission } = plan;
|
|
944
|
+
|
|
945
|
+
// Recruit via HR assistant
|
|
946
|
+
const agents = this.secretary.hrAssistant.executeRecruitment(teamPlan, this.hr);
|
|
947
|
+
|
|
948
|
+
// Create department
|
|
949
|
+
const dept = new Department({ name, mission, company: this.id });
|
|
950
|
+
const wsPath = this.workspaceManager.createDepartmentWorkspace(dept.id, name);
|
|
951
|
+
dept.workspacePath = wsPath;
|
|
952
|
+
|
|
953
|
+
// Add to department + initialize toolkits
|
|
954
|
+
agents.forEach(agent => {
|
|
955
|
+
dept.addAgent(agent);
|
|
956
|
+
agent.initToolKit(wsPath, this.messageBus);
|
|
957
|
+
});
|
|
958
|
+
|
|
959
|
+
// Set department leader
|
|
960
|
+
const leader = agents.find(a => a.role === 'Project Leader');
|
|
961
|
+
if (leader) {
|
|
962
|
+
dept.setLeader(leader);
|
|
963
|
+
} else if (agents.length > 0) {
|
|
964
|
+
dept.setLeader(agents[0]);
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
this.departments.set(dept.id, dept);
|
|
968
|
+
|
|
969
|
+
this._log('Department created', `"${name}" department established, recruited ${agents.length} talents`);
|
|
970
|
+
|
|
971
|
+
// Fire hooks: department created + agents created
|
|
972
|
+
hookRegistry.trigger(HookEvent.DEPT_CREATED, {
|
|
973
|
+
departmentId: dept.id, departmentName: dept.name, memberCount: agents.length,
|
|
974
|
+
});
|
|
975
|
+
for (const agent of agents) {
|
|
976
|
+
hookRegistry.trigger(HookEvent.AGENT_CREATED, {
|
|
977
|
+
agentId: agent.id, agentName: agent.name, role: agent.role,
|
|
978
|
+
departmentId: dept.id, departmentName: dept.name,
|
|
979
|
+
});
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
// Background async: Agent self-intro + onboarding email + broadcast
|
|
983
|
+
this._onboardAgents(agents, dept).catch(e => console.error('Onboarding process error:', e));
|
|
984
|
+
|
|
985
|
+
// Start group chat loop for new employee
|
|
986
|
+
for (const agent of agents) {
|
|
987
|
+
groupChatLoop.startAgentLoop(agent);
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
// Persist
|
|
991
|
+
this.save();
|
|
992
|
+
|
|
993
|
+
return dept;
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
/**
|
|
997
|
+
* Create department directly from secretary's team plan (no second AI call).
|
|
998
|
+
* The secretary already designed the team in the create_department action.
|
|
999
|
+
* @param {object} params
|
|
1000
|
+
* @param {string} params.departmentName - Department name
|
|
1001
|
+
* @param {string} params.mission - Department mission
|
|
1002
|
+
* @param {Array} params.members - Team members from secretary's plan
|
|
1003
|
+
* @returns {Promise<Department>} Created department
|
|
1004
|
+
*/
|
|
1005
|
+
async createDepartmentDirect({ departmentName, mission, members }) {
|
|
1006
|
+
const name = departmentName || 'New Project Dept';
|
|
1007
|
+
|
|
1008
|
+
// Validate and normalize members against JobTemplates
|
|
1009
|
+
const validTemplateIds = new Set(Object.values(JobTemplates).map(t => t.id));
|
|
1010
|
+
const validMembers = (members || []).filter(m => validTemplateIds.has(m.templateId));
|
|
1011
|
+
|
|
1012
|
+
if (validMembers.length === 0) {
|
|
1013
|
+
// Fallback: if secretary returned no valid members, fall back to planDepartment
|
|
1014
|
+
console.warn('⚠️ Secretary returned no valid members, falling back to planDepartment');
|
|
1015
|
+
const plan = await this.planDepartment(name, mission);
|
|
1016
|
+
return await this.confirmPlan(plan.planId);
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
// Build teamPlan in the same format as designTeam returns
|
|
1020
|
+
const teamPlan = {
|
|
1021
|
+
departmentName: name,
|
|
1022
|
+
mission,
|
|
1023
|
+
members: validMembers.map((m, i) => {
|
|
1024
|
+
const template = Object.values(JobTemplates).find(t => t.id === m.templateId);
|
|
1025
|
+
return {
|
|
1026
|
+
templateId: m.templateId,
|
|
1027
|
+
templateTitle: template?.title || m.templateId,
|
|
1028
|
+
name: m.name || `Employee${i + 1}`,
|
|
1029
|
+
isLeader: m.isLeader || false,
|
|
1030
|
+
reportsTo: m.reportsTo ?? (i === 0 ? null : 0),
|
|
1031
|
+
reason: m.reason || '',
|
|
1032
|
+
};
|
|
1033
|
+
}),
|
|
1034
|
+
};
|
|
1035
|
+
|
|
1036
|
+
console.log(`📋 Secretary's direct team plan: ${name}, ${teamPlan.members.length} people`);
|
|
1037
|
+
teamPlan.members.forEach(m => {
|
|
1038
|
+
const prefix = m.isLeader ? '👔' : '👤';
|
|
1039
|
+
console.log(` ${prefix} ${m.name} - ${m.templateTitle}`);
|
|
1040
|
+
});
|
|
1041
|
+
|
|
1042
|
+
// Recruit via HR assistant
|
|
1043
|
+
const agents = this.secretary.hrAssistant.executeRecruitment(teamPlan, this.hr);
|
|
1044
|
+
|
|
1045
|
+
// Create department
|
|
1046
|
+
const dept = new Department({ name, mission, company: this.id });
|
|
1047
|
+
const wsPath = this.workspaceManager.createDepartmentWorkspace(dept.id, name);
|
|
1048
|
+
dept.workspacePath = wsPath;
|
|
1049
|
+
|
|
1050
|
+
// Add to department + initialize toolkits
|
|
1051
|
+
agents.forEach(agent => {
|
|
1052
|
+
dept.addAgent(agent);
|
|
1053
|
+
agent.initToolKit(wsPath, this.messageBus);
|
|
1054
|
+
});
|
|
1055
|
+
|
|
1056
|
+
// Set department leader
|
|
1057
|
+
const leader = agents.find(a => a.role === 'Project Leader');
|
|
1058
|
+
if (leader) {
|
|
1059
|
+
dept.setLeader(leader);
|
|
1060
|
+
} else if (agents.length > 0) {
|
|
1061
|
+
dept.setLeader(agents[0]);
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
this.departments.set(dept.id, dept);
|
|
1065
|
+
|
|
1066
|
+
this._log('Department created', `"${name}" department established, recruited ${agents.length} talents`);
|
|
1067
|
+
|
|
1068
|
+
// Fire hooks
|
|
1069
|
+
hookRegistry.trigger(HookEvent.DEPT_CREATED, {
|
|
1070
|
+
departmentId: dept.id, departmentName: dept.name, memberCount: agents.length,
|
|
1071
|
+
});
|
|
1072
|
+
for (const agent of agents) {
|
|
1073
|
+
hookRegistry.trigger(HookEvent.AGENT_CREATED, {
|
|
1074
|
+
agentId: agent.id, agentName: agent.name, role: agent.role,
|
|
1075
|
+
departmentId: dept.id, departmentName: dept.name,
|
|
1076
|
+
});
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
// Background: Agent self-intro + onboarding
|
|
1080
|
+
this._onboardAgents(agents, dept).catch(e => console.error('Onboarding process error:', e));
|
|
1081
|
+
|
|
1082
|
+
// Start group chat loop
|
|
1083
|
+
for (const agent of agents) {
|
|
1084
|
+
groupChatLoop.startAgentLoop(agent);
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
this.save();
|
|
1088
|
+
|
|
1089
|
+
return dept;
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
/**
|
|
1093
|
+
* Employee onboarding flow: generate self-intro + send onboarding email + broadcast
|
|
1094
|
+
*/
|
|
1095
|
+
async _onboardAgents(agents, dept) {
|
|
1096
|
+
for (const agent of agents) {
|
|
1097
|
+
// Let the employee introduce themselves — this is THEIR moment, not secretary's
|
|
1098
|
+
const onboardResult = await agent.onboard({
|
|
1099
|
+
departmentName: dept.name,
|
|
1100
|
+
bossName: this.bossName,
|
|
1101
|
+
});
|
|
1102
|
+
|
|
1103
|
+
// Send greeting to boss (employee's own words, or fallback template)
|
|
1104
|
+
const greetingContent = onboardResult.greeting
|
|
1105
|
+
|| `Hi ${this.bossName}, I'm ${agent.name}, just joined "${dept.name}" as ${agent.role}. My motto: "${agent.signature}". Looking forward to working with you!`;
|
|
1106
|
+
agent.sendMailToBoss(null, greetingContent, this);
|
|
1107
|
+
|
|
1108
|
+
// Broadcast to colleagues (employee's own words, or fallback)
|
|
1109
|
+
const allAgentIds = [];
|
|
1110
|
+
this.departments.forEach(d => {
|
|
1111
|
+
d.getMembers().forEach(a => {
|
|
1112
|
+
if (a.id !== agent.id) allAgentIds.push(a.id);
|
|
1113
|
+
});
|
|
1114
|
+
});
|
|
1115
|
+
if (allAgentIds.length > 0) {
|
|
1116
|
+
const broadcastContent = onboardResult.broadcast
|
|
1117
|
+
|| `👋 Hi everyone, I'm ${agent.name}, the new ${agent.role} in "${dept.name}". Nice to meet you all!`;
|
|
1118
|
+
this.messageBus.broadcast(agent.id, allAgentIds, broadcastContent, 'broadcast');
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
/**
|
|
1124
|
+
* General department lookup: try ID first, then fuzzy match by name
|
|
1125
|
+
* @param {string} idOrName - Department ID or name
|
|
1126
|
+
* @returns {Department|null}
|
|
1127
|
+
*/
|
|
1128
|
+
findDepartment(idOrName) {
|
|
1129
|
+
if (!idOrName) return null;
|
|
1130
|
+
// Prioritize exact ID match
|
|
1131
|
+
const byId = this.departments.get(idOrName);
|
|
1132
|
+
if (byId) return byId;
|
|
1133
|
+
// Fallback: match by name
|
|
1134
|
+
for (const d of this.departments.values()) {
|
|
1135
|
+
if (d.name === idOrName || d.name.includes(idOrName) || idOrName.includes(d.name)) {
|
|
1136
|
+
console.log(`🔧 Matched department by name: "${idOrName}" → ${d.id} (${d.name})`);
|
|
1137
|
+
return d;
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
return null;
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
createDepartment(name, mission) {
|
|
1144
|
+
const dept = new Department({ name, mission, company: this.id });
|
|
1145
|
+
const wsPath = this.workspaceManager.createDepartmentWorkspace(dept.id, name);
|
|
1146
|
+
dept.workspacePath = wsPath;
|
|
1147
|
+
this.departments.set(dept.id, dept);
|
|
1148
|
+
return dept;
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
hireAgent(departmentId, templateId, name, providerId = null) {
|
|
1152
|
+
const dept = this.findDepartment(departmentId);
|
|
1153
|
+
if (!dept) throw new Error(`Department not found: ${departmentId}`);
|
|
1154
|
+
|
|
1155
|
+
const recruitConfig = this.hr.recruit(templateId, name, providerId);
|
|
1156
|
+
const agent = createEmployee(recruitConfig);
|
|
1157
|
+
dept.addAgent(agent);
|
|
1158
|
+
|
|
1159
|
+
// Initialize toolkit
|
|
1160
|
+
if (dept.workspacePath) {
|
|
1161
|
+
agent.initToolKit(dept.workspacePath, this.messageBus);
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
// Start group chat loop
|
|
1165
|
+
groupChatLoop.startAgentLoop(agent);
|
|
1166
|
+
|
|
1167
|
+
return agent;
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
recallAgent(departmentId, profileId, newSkills = []) {
|
|
1171
|
+
const dept = this.findDepartment(departmentId);
|
|
1172
|
+
if (!dept) throw new Error(`Department not found: ${departmentId}`);
|
|
1173
|
+
|
|
1174
|
+
const recruitConfig = this.hr.recallFromMarket(profileId, newSkills);
|
|
1175
|
+
const agent = createEmployee(recruitConfig);
|
|
1176
|
+
agent.memory.addLongTerm(
|
|
1177
|
+
`Recalled to the "${dept.name}" department, carrying past experience and memories back to work`,
|
|
1178
|
+
'experience'
|
|
1179
|
+
);
|
|
1180
|
+
dept.addAgent(agent);
|
|
1181
|
+
|
|
1182
|
+
if (dept.workspacePath) {
|
|
1183
|
+
agent.initToolKit(dept.workspacePath, this.messageBus);
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
// Start group chat loop for recalled employee
|
|
1187
|
+
groupChatLoop.startAgentLoop(agent);
|
|
1188
|
+
|
|
1189
|
+
console.log(` 🔄 [${agent.name}] Recalled from talent market, joined "${dept.name}" department`);
|
|
1190
|
+
return agent;
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
dismissAgent(departmentId, agentId, reason = 'Project ended') {
|
|
1194
|
+
const dept = this.findDepartment(departmentId);
|
|
1195
|
+
if (!dept) throw new Error(`Department not found: ${departmentId}`);
|
|
1196
|
+
|
|
1197
|
+
const agent = dept.removeAgent(agentId);
|
|
1198
|
+
if (!agent) throw new Error(`Employee not found: ${agentId}`);
|
|
1199
|
+
|
|
1200
|
+
agent.status = 'dismissed';
|
|
1201
|
+
|
|
1202
|
+
// Stop group chat loop
|
|
1203
|
+
groupChatLoop.stopAgentLoop(agentId);
|
|
1204
|
+
|
|
1205
|
+
// Fire hook: agent dismissed
|
|
1206
|
+
hookRegistry.trigger(HookEvent.AGENT_DISMISSED, {
|
|
1207
|
+
agentId: agent.id, agentName: agent.name, role: agent.role,
|
|
1208
|
+
departmentId: departmentId, reason,
|
|
1209
|
+
});
|
|
1210
|
+
|
|
1211
|
+
const performanceData = {
|
|
1212
|
+
reviews: this.performanceSystem.getReviews(agentId),
|
|
1213
|
+
averageScore: this.performanceSystem.getAverageScore(agentId),
|
|
1214
|
+
};
|
|
1215
|
+
|
|
1216
|
+
const profile = this.talentMarket.register(agent, reason, performanceData);
|
|
1217
|
+
|
|
1218
|
+
|
|
1219
|
+
|
|
1220
|
+
// Clean up message bus inbox
|
|
1221
|
+
this.messageBus.clearInbox(agentId);
|
|
1222
|
+
|
|
1223
|
+
console.log(` 📤 [${agent.name}] has been dismissed, entered talent market`);
|
|
1224
|
+
return profile;
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
/**
|
|
1228
|
+
* Permanently delete a talent from the talent market, and clean up all their messages in mailbox and message bus
|
|
1229
|
+
* @param {string} profileId - Talent market profile ID
|
|
1230
|
+
*/
|
|
1231
|
+
deleteTalent(profileId) {
|
|
1232
|
+
const profile = this.talentMarket.remove(profileId);
|
|
1233
|
+
const originalAgentId = profile.originalAgentId;
|
|
1234
|
+
|
|
1235
|
+
// Clean up mailbox messages from this person
|
|
1236
|
+
this.mailbox = this.mailbox.filter(m => m.from?.id !== originalAgentId);
|
|
1237
|
+
|
|
1238
|
+
// Clean up message bus messages (sent and received)
|
|
1239
|
+
this.messageBus.messages = this.messageBus.messages.filter(
|
|
1240
|
+
m => m.from !== originalAgentId && m.to !== originalAgentId
|
|
1241
|
+
);
|
|
1242
|
+
this.messageBus.inbox.delete(originalAgentId);
|
|
1243
|
+
|
|
1244
|
+
this._log('Delete talent', `Permanently deleted "${profile.name}" from talent market and cleaned up related messages`);
|
|
1245
|
+
this.save();
|
|
1246
|
+
return profile;
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
/**
|
|
1250
|
+
* Adjust department staffing - Step 1: Get adjustment plan
|
|
1251
|
+
* @param {string} departmentId - Department ID
|
|
1252
|
+
* @param {string} adjustGoal - Adjustment goal
|
|
1253
|
+
* @returns {object} Adjustment plan (pending approval)
|
|
1254
|
+
*/
|
|
1255
|
+
async planAdjustment(departmentId, adjustGoal) {
|
|
1256
|
+
const dept = this.findDepartment(departmentId);
|
|
1257
|
+
if (!dept) throw new Error(`Department not found: ${departmentId}`);
|
|
1258
|
+
|
|
1259
|
+
const adjustPlan = await dept.adjustTeam(adjustGoal, this.secretary, this.providerRegistry);
|
|
1260
|
+
|
|
1261
|
+
const planId = uuidv4();
|
|
1262
|
+
this.pendingPlans.set(planId, {
|
|
1263
|
+
type: 'adjustment',
|
|
1264
|
+
departmentId,
|
|
1265
|
+
departmentName: dept.name,
|
|
1266
|
+
adjustGoal,
|
|
1267
|
+
adjustPlan,
|
|
1268
|
+
});
|
|
1269
|
+
|
|
1270
|
+
this._log('Adjustment plan', `Secretary created adjustment plan for "${dept.name}": fire ${adjustPlan.fires.length}, hire ${adjustPlan.hires.length}, pending approval`);
|
|
1271
|
+
|
|
1272
|
+
return {
|
|
1273
|
+
planId,
|
|
1274
|
+
departmentId,
|
|
1275
|
+
departmentName: dept.name,
|
|
1276
|
+
adjustGoal,
|
|
1277
|
+
reasoning: adjustPlan.reasoning,
|
|
1278
|
+
fires: adjustPlan.fires,
|
|
1279
|
+
hires: adjustPlan.hires.map(h => {
|
|
1280
|
+
// Find recommended provider info
|
|
1281
|
+
const template = this.hr.getTemplate(h.templateId);
|
|
1282
|
+
let providerName = null;
|
|
1283
|
+
let providerModel = null;
|
|
1284
|
+
if (template) {
|
|
1285
|
+
const recommended = this.providerRegistry.recommend(
|
|
1286
|
+
template.category,
|
|
1287
|
+
template.requiredCapabilities
|
|
1288
|
+
);
|
|
1289
|
+
if (recommended) {
|
|
1290
|
+
providerName = recommended.name;
|
|
1291
|
+
providerModel = recommended.model;
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1294
|
+
return { ...h, providerName, providerModel };
|
|
1295
|
+
}),
|
|
1296
|
+
};
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
/**
|
|
1300
|
+
* Confirm adjustment plan - Step 2: Execute adjustment
|
|
1301
|
+
* @param {string} planId - Adjustment plan ID
|
|
1302
|
+
*/
|
|
1303
|
+
async confirmAdjustment(planId) {
|
|
1304
|
+
const plan = this.pendingPlans.get(planId);
|
|
1305
|
+
if (!plan || plan.type !== 'adjustment') throw new Error('Adjustment plan not found or expired');
|
|
1306
|
+
|
|
1307
|
+
this.pendingPlans.delete(planId);
|
|
1308
|
+
|
|
1309
|
+
const dept = this.departments.get(plan.departmentId);
|
|
1310
|
+
if (!dept) throw new Error(`Department not found: ${plan.departmentId}`);
|
|
1311
|
+
|
|
1312
|
+
const { adjustPlan } = plan;
|
|
1313
|
+
|
|
1314
|
+
// Execute layoffs
|
|
1315
|
+
for (const fire of adjustPlan.fires) {
|
|
1316
|
+
try {
|
|
1317
|
+
this.dismissAgent(plan.departmentId, fire.agentId, fire.reason || 'Department restructuring');
|
|
1318
|
+
this._log('Adjustment layoff', `"${plan.departmentName}": ${fire.name} laid off - ${fire.reason || 'Department restructuring'}`);
|
|
1319
|
+
} catch (e) {
|
|
1320
|
+
console.error(`Layoff failed [${fire.name}]:`, e.message);
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
// Execute hiring
|
|
1325
|
+
const newAgents = [];
|
|
1326
|
+
if (adjustPlan.hires.length > 0) {
|
|
1327
|
+
// Construct in same format as designTeam plan
|
|
1328
|
+
const hirePlan = {
|
|
1329
|
+
members: adjustPlan.hires.map(h => ({
|
|
1330
|
+
templateId: h.templateId,
|
|
1331
|
+
templateTitle: h.templateTitle || h.templateId,
|
|
1332
|
+
name: h.name,
|
|
1333
|
+
isLeader: h.isLeader || false,
|
|
1334
|
+
reportsTo: h.reportsTo ?? 0,
|
|
1335
|
+
reason: h.reason,
|
|
1336
|
+
})),
|
|
1337
|
+
};
|
|
1338
|
+
|
|
1339
|
+
const agents = this.secretary.hrAssistant.executeRecruitment(hirePlan, this.hr);
|
|
1340
|
+
for (const agent of agents) {
|
|
1341
|
+
if (!agent) continue;
|
|
1342
|
+
dept.addAgent(agent);
|
|
1343
|
+
if (dept.workspacePath) {
|
|
1344
|
+
agent.initToolKit(dept.workspacePath, this.messageBus);
|
|
1345
|
+
}
|
|
1346
|
+
newAgents.push(agent);
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
if (newAgents.length > 0) {
|
|
1350
|
+
this._log('Adjustment hire', `"${plan.departmentName}": hired ${newAgents.length} new employees`);
|
|
1351
|
+
// Background async onboarding
|
|
1352
|
+
this._onboardAgents(newAgents, dept).catch(e => console.error('Onboarding process error:', e));
|
|
1353
|
+
// Start group chat loop for new employee
|
|
1354
|
+
for (const agent of newAgents) {
|
|
1355
|
+
groupChatLoop.startAgentLoop(agent);
|
|
1356
|
+
}
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
this.save();
|
|
1361
|
+
return dept;
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1364
|
+
/**
|
|
1365
|
+
* Disband department - all members enter talent market
|
|
1366
|
+
* @param {string} departmentId - Department ID
|
|
1367
|
+
* @param {string} reason - Disbanding reason
|
|
1368
|
+
*/
|
|
1369
|
+
disbandDepartment(departmentId, reason = 'Organizational restructuring') {
|
|
1370
|
+
const dept = this.findDepartment(departmentId);
|
|
1371
|
+
if (!dept) throw new Error(`Department not found: ${departmentId}`);
|
|
1372
|
+
|
|
1373
|
+
const deptName = dept.name;
|
|
1374
|
+
const members = dept.getMembers();
|
|
1375
|
+
|
|
1376
|
+
// Dismiss all members one by one
|
|
1377
|
+
for (const agent of members) {
|
|
1378
|
+
try {
|
|
1379
|
+
this.dismissAgent(departmentId, agent.id, `Department "${deptName}" disbanded: ${reason}`);
|
|
1380
|
+
} catch (e) {
|
|
1381
|
+
console.error(`Dismissal failed [${agent.name}]:`, e.message);
|
|
1382
|
+
}
|
|
1383
|
+
}
|
|
1384
|
+
|
|
1385
|
+
// Delete department
|
|
1386
|
+
this.departments.delete(departmentId);
|
|
1387
|
+
|
|
1388
|
+
this._log('Department disbanded', `"${deptName}" department disbanded, ${members.length} employees entered talent market. Reason: ${reason}`);
|
|
1389
|
+
this.save();
|
|
1390
|
+
|
|
1391
|
+
return { departmentName: deptName, dismissedCount: members.length };
|
|
1392
|
+
}
|
|
1393
|
+
|
|
1394
|
+
evaluateAgent(agentId, reviewerId, taskTitle, scores = null, comment = null) {
|
|
1395
|
+
let agent = null;
|
|
1396
|
+
let reviewer = null;
|
|
1397
|
+
|
|
1398
|
+
for (const dept of this.departments.values()) {
|
|
1399
|
+
if (!agent) agent = dept.agents.get(agentId);
|
|
1400
|
+
if (!reviewer) reviewer = dept.agents.get(reviewerId);
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1403
|
+
if (!agent) throw new Error(`Employee not found: ${agentId}`);
|
|
1404
|
+
if (!reviewer) throw new Error(`Reviewer not found: ${reviewerId}`);
|
|
1405
|
+
|
|
1406
|
+
let review;
|
|
1407
|
+
if (scores) {
|
|
1408
|
+
review = this.performanceSystem.evaluate({
|
|
1409
|
+
agent, reviewer, taskTitle, scores, comment,
|
|
1410
|
+
});
|
|
1411
|
+
} else {
|
|
1412
|
+
review = this.performanceSystem.autoEvaluate({
|
|
1413
|
+
agent, reviewer, taskTitle,
|
|
1414
|
+
});
|
|
1415
|
+
}
|
|
1416
|
+
|
|
1417
|
+
agent.receiveFeedback(review);
|
|
1418
|
+
return review;
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1421
|
+
viewTalentMarket() {
|
|
1422
|
+
this.talentMarket.print();
|
|
1423
|
+
return this.talentMarket.listAvailable();
|
|
1424
|
+
}
|
|
1425
|
+
|
|
1426
|
+
searchTalentMarket(criteria) {
|
|
1427
|
+
return this.talentMarket.search(criteria);
|
|
1428
|
+
}
|
|
1429
|
+
|
|
1430
|
+
viewPerformanceReport(agentId, agentName = '') {
|
|
1431
|
+
this.performanceSystem.printReport(agentId, agentName);
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1434
|
+
viewAgentMemory(agentId) {
|
|
1435
|
+
for (const dept of this.departments.values()) {
|
|
1436
|
+
const agent = dept.agents.get(agentId);
|
|
1437
|
+
if (agent) {
|
|
1438
|
+
agent.memory.print(agent.name);
|
|
1439
|
+
return agent.memory.getSummary();
|
|
1440
|
+
}
|
|
1441
|
+
}
|
|
1442
|
+
throw new Error(`Employee not found: ${agentId}`);
|
|
1443
|
+
}
|
|
1444
|
+
|
|
1445
|
+
getOverview() {
|
|
1446
|
+
const overview = {
|
|
1447
|
+
company: this.name,
|
|
1448
|
+
boss: this.bossName,
|
|
1449
|
+
departments: [],
|
|
1450
|
+
totalAgents: 0,
|
|
1451
|
+
talentMarket: this.talentMarket.getStats(),
|
|
1452
|
+
};
|
|
1453
|
+
|
|
1454
|
+
this.departments.forEach(dept => {
|
|
1455
|
+
const summary = dept.getSummary();
|
|
1456
|
+
overview.departments.push(summary);
|
|
1457
|
+
overview.totalAgents += summary.memberCount;
|
|
1458
|
+
});
|
|
1459
|
+
|
|
1460
|
+
return overview;
|
|
1461
|
+
}
|
|
1462
|
+
|
|
1463
|
+
listProviders(category = null) {
|
|
1464
|
+
if (category) {
|
|
1465
|
+
return this.providerRegistry.getByCategory(category);
|
|
1466
|
+
}
|
|
1467
|
+
return this.providerRegistry.listAll();
|
|
1468
|
+
}
|
|
1469
|
+
|
|
1470
|
+
listJobTemplates(category = null) {
|
|
1471
|
+
if (category) {
|
|
1472
|
+
return this.hr.listTemplatesByCategory(category);
|
|
1473
|
+
}
|
|
1474
|
+
return this.hr.listAllTemplates();
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1477
|
+
configureProvider(providerId, apiKey) {
|
|
1478
|
+
const provider = this.providerRegistry.configure(providerId, apiKey);
|
|
1479
|
+
// Sync with the appropriate client
|
|
1480
|
+
if (provider.isWeb) {
|
|
1481
|
+
// For web providers, apiKey carries the session token (legacy cookie field)
|
|
1482
|
+
webClientRegistry.configureCookie(providerId, apiKey);
|
|
1483
|
+
} else {
|
|
1484
|
+
// Clear LLM client cache to ensure next call uses new apiKey
|
|
1485
|
+
llmClient.clearClient(providerId);
|
|
1486
|
+
}
|
|
1487
|
+
this._log('Configure provider', `${provider.name} has been ${apiKey ? 'enabled' : 'disabled'}`);
|
|
1488
|
+
return provider;
|
|
1489
|
+
}
|
|
1490
|
+
|
|
1491
|
+
getProviderDashboard() {
|
|
1492
|
+
return this.providerRegistry.getStats();
|
|
1493
|
+
}
|
|
1494
|
+
|
|
1495
|
+
/**
|
|
1496
|
+
* Assign task to department and let employees actually execute it
|
|
1497
|
+
* This is the core method that makes AI employees "actually work"
|
|
1498
|
+
* Uses requirement management: standardize requirement → leader decomposes workflow → execute by DAG → group chat communication
|
|
1499
|
+
* @param {string} departmentId - Target department ID
|
|
1500
|
+
* @param {string} taskDescription - Task description
|
|
1501
|
+
* @param {string} [taskTitle] - Task title
|
|
1502
|
+
* @returns {Promise<object>} Execution result
|
|
1503
|
+
*/
|
|
1504
|
+
async assignTaskToDepartment(departmentId, taskDescription, taskTitle = null) {
|
|
1505
|
+
const dept = this.findDepartment(departmentId);
|
|
1506
|
+
if (!dept) throw new Error(`Department not found: ${departmentId}`);
|
|
1507
|
+
|
|
1508
|
+
const members = dept.getMembers();
|
|
1509
|
+
if (members.length === 0) throw new Error(`Department "${dept.name}" has no employees`);
|
|
1510
|
+
|
|
1511
|
+
const title = taskTitle || taskDescription.slice(0, 50);
|
|
1512
|
+
this._log('Task assigned', `"${dept.name}" received task: "${title}"`);
|
|
1513
|
+
|
|
1514
|
+
// Fire hook: task assigned
|
|
1515
|
+
hookRegistry.trigger(HookEvent.TASK_ASSIGNED, {
|
|
1516
|
+
departmentId: dept.id, departmentName: dept.name, taskTitle: title,
|
|
1517
|
+
});
|
|
1518
|
+
|
|
1519
|
+
// 1. Create standardized requirement
|
|
1520
|
+
const requirement = this.requirementManager.create({
|
|
1521
|
+
title,
|
|
1522
|
+
description: taskDescription,
|
|
1523
|
+
departmentId: dept.id,
|
|
1524
|
+
departmentName: dept.name,
|
|
1525
|
+
bossMessage: taskDescription,
|
|
1526
|
+
});
|
|
1527
|
+
|
|
1528
|
+
// Persist immediately to prevent data loss after requirement creation
|
|
1529
|
+
this.save();
|
|
1530
|
+
console.log(`📝 Requirement created: ${requirement.id} - ${title}`);
|
|
1531
|
+
|
|
1532
|
+
// 2. Leader decomposes workflow
|
|
1533
|
+
const leader = dept.getLeader() || members[0];
|
|
1534
|
+
|
|
1535
|
+
// Update liveStatus: record leader info during planning phase
|
|
1536
|
+
requirement.updateLiveStatus({
|
|
1537
|
+
currentAgent: leader.name,
|
|
1538
|
+
currentAgentId: leader.id,
|
|
1539
|
+
currentAgentAvatar: leader.avatar,
|
|
1540
|
+
currentAction: `${leader.name} is analyzing and decomposing the requirement...`,
|
|
1541
|
+
});
|
|
1542
|
+
this.save();
|
|
1543
|
+
|
|
1544
|
+
try {
|
|
1545
|
+
await this.requirementManager.planWorkflow(
|
|
1546
|
+
requirement, members
|
|
1547
|
+
);
|
|
1548
|
+
} catch (e) {
|
|
1549
|
+
console.error('Workflow decomposition failed:', e.message);
|
|
1550
|
+
// Save current state even if decomposition fails (fallback workflow is set inside planWorkflow)
|
|
1551
|
+
}
|
|
1552
|
+
|
|
1553
|
+
// Save again after workflow decomposition
|
|
1554
|
+
this.save();
|
|
1555
|
+
|
|
1556
|
+
// 3. Execute by workflow DAG
|
|
1557
|
+
let summary;
|
|
1558
|
+
try {
|
|
1559
|
+
summary = await this.requirementManager.executeWorkflow(
|
|
1560
|
+
requirement, dept, this.performanceSystem
|
|
1561
|
+
);
|
|
1562
|
+
} catch (e) {
|
|
1563
|
+
console.error('Workflow execution failed:', e.message);
|
|
1564
|
+
// Update requirement status to failed
|
|
1565
|
+
requirement.status = 'failed';
|
|
1566
|
+
requirement.completedAt = new Date();
|
|
1567
|
+
requirement.summary = { totalTasks: 0, successTasks: 0, failedTasks: 0, totalDuration: 0, outputs: [], error: e.message };
|
|
1568
|
+
requirement.addGroupMessage(
|
|
1569
|
+
{ name: 'System', role: 'system' },
|
|
1570
|
+
`❌ Requirement execution failed: ${e.message}`,
|
|
1571
|
+
'system', null, { auto: true }
|
|
1572
|
+
);
|
|
1573
|
+
this.save();
|
|
1574
|
+
summary = requirement.summary;
|
|
1575
|
+
}
|
|
1576
|
+
|
|
1577
|
+
// 4. Let leader send report email
|
|
1578
|
+
if (leader) {
|
|
1579
|
+
let reportContent = `Requirement "${title}" completed!\n\n`;
|
|
1580
|
+
reportContent += `📊 Execution Summary:\n`;
|
|
1581
|
+
reportContent += `- Tasks completed: ${summary.successTasks}/${summary.totalTasks}\n`;
|
|
1582
|
+
reportContent += `- Total duration: ${Math.round(summary.totalDuration / 1000)}s\n\n`;
|
|
1583
|
+
reportContent += `📝 Member outputs:\n`;
|
|
1584
|
+
for (const o of (summary.outputs || [])) {
|
|
1585
|
+
reportContent += `\n[${o.agentName} (${o.role})]\n`;
|
|
1586
|
+
reportContent += (o.content || '').slice(0, 300);
|
|
1587
|
+
if ((o.content || '').length > 300) reportContent += '...';
|
|
1588
|
+
reportContent += '\n';
|
|
1589
|
+
}
|
|
1590
|
+
leader.sendMailToBoss(`📋 Requirement Report: ${title}`, reportContent, this);
|
|
1591
|
+
}
|
|
1592
|
+
|
|
1593
|
+
// 5. Record to progress reports
|
|
1594
|
+
this.progressReports.push({
|
|
1595
|
+
time: new Date(),
|
|
1596
|
+
type: 'task_completed',
|
|
1597
|
+
reports: [{
|
|
1598
|
+
department: dept.name,
|
|
1599
|
+
task: title,
|
|
1600
|
+
requirementId: requirement.id,
|
|
1601
|
+
success: summary.successTasks === summary.totalTasks,
|
|
1602
|
+
detail: `${summary.successTasks}/${summary.totalTasks} subtasks completed, took ${Math.round(summary.totalDuration / 1000)}s`,
|
|
1603
|
+
}],
|
|
1604
|
+
});
|
|
1605
|
+
|
|
1606
|
+
// Fire hook: task completed
|
|
1607
|
+
hookRegistry.trigger(HookEvent.TASK_COMPLETED, {
|
|
1608
|
+
departmentId: dept.id, departmentName: dept.name, taskTitle: title,
|
|
1609
|
+
totalTasks: summary.totalTasks, successTasks: summary.successTasks,
|
|
1610
|
+
});
|
|
1611
|
+
|
|
1612
|
+
this._log('Task completed', `"${dept.name}" completed task: "${title}", ${summary.successTasks}/${summary.totalTasks} succeeded`);
|
|
1613
|
+
this.save();
|
|
1614
|
+
|
|
1615
|
+
// Return summary with requirement ID
|
|
1616
|
+
return {
|
|
1617
|
+
requirementId: requirement.id,
|
|
1618
|
+
projectId: requirement.id,
|
|
1619
|
+
title,
|
|
1620
|
+
department: dept.name,
|
|
1621
|
+
departmentId: dept.id,
|
|
1622
|
+
totalTasks: summary.totalTasks,
|
|
1623
|
+
successTasks: summary.successTasks,
|
|
1624
|
+
failedTasks: summary.failedTasks,
|
|
1625
|
+
totalDuration: summary.totalDuration,
|
|
1626
|
+
outputs: (summary.outputs || []).map(o => ({
|
|
1627
|
+
agentName: o.agentName,
|
|
1628
|
+
role: o.role,
|
|
1629
|
+
output: o.content,
|
|
1630
|
+
outputType: o.outputType,
|
|
1631
|
+
toolResults: o.metadata?.toolResults || [],
|
|
1632
|
+
success: true,
|
|
1633
|
+
duration: o.metadata?.duration || 0,
|
|
1634
|
+
})),
|
|
1635
|
+
completedAt: new Date(),
|
|
1636
|
+
};
|
|
1637
|
+
}
|
|
1638
|
+
|
|
1639
|
+
/**
|
|
1640
|
+
* Convert an approved Sprint into a standard Requirement and execute it.
|
|
1641
|
+
* Only team members participate (not the entire department).
|
|
1642
|
+
* @param {object} sprint - Sprint object
|
|
1643
|
+
* @param {object} team - Team object
|
|
1644
|
+
* @returns {Promise<object>} The created requirement
|
|
1645
|
+
*/
|
|
1646
|
+
async assignSprintAsDepartmentTask(sprint, team) {
|
|
1647
|
+
const dept = this.findDepartment(team.departmentId);
|
|
1648
|
+
if (!dept) throw new Error(`Department not found: ${team.departmentId}`);
|
|
1649
|
+
|
|
1650
|
+
const members = team.memberIds.map(mid => dept.agents.get(mid)).filter(Boolean);
|
|
1651
|
+
if (members.length === 0) throw new Error(`Team "${team.name}" has no valid members`);
|
|
1652
|
+
|
|
1653
|
+
const leader = dept.agents.get(team.leaderId) || members[0];
|
|
1654
|
+
|
|
1655
|
+
// Build task description from sprint plan + goal
|
|
1656
|
+
const taskDescription = sprint.plan
|
|
1657
|
+
? `# Sprint Goal\n${sprint.goal}\n\n# Implementation Plan\n${sprint.plan}`
|
|
1658
|
+
: sprint.goal;
|
|
1659
|
+
const title = sprint.title;
|
|
1660
|
+
|
|
1661
|
+
this._log('Sprint → Requirement', `Team "${team.name}" sprint "${title}" approved, creating requirement`);
|
|
1662
|
+
|
|
1663
|
+
// 1. Create standard requirement
|
|
1664
|
+
const requirement = this.requirementManager.create({
|
|
1665
|
+
title,
|
|
1666
|
+
description: taskDescription,
|
|
1667
|
+
departmentId: dept.id,
|
|
1668
|
+
departmentName: dept.name,
|
|
1669
|
+
bossMessage: sprint.goal,
|
|
1670
|
+
});
|
|
1671
|
+
|
|
1672
|
+
// Link sprint ↔ requirement
|
|
1673
|
+
sprint.requirementId = requirement.id;
|
|
1674
|
+
this.save();
|
|
1675
|
+
console.log(`📝 Sprint → Requirement created: ${requirement.id} - ${title}`);
|
|
1676
|
+
|
|
1677
|
+
// 2. Leader decomposes workflow (using only team members, not entire dept)
|
|
1678
|
+
requirement.updateLiveStatus({
|
|
1679
|
+
currentAgent: leader.name,
|
|
1680
|
+
currentAgentId: leader.id,
|
|
1681
|
+
currentAgentAvatar: leader.avatar,
|
|
1682
|
+
currentAction: `${leader.name} is analyzing and decomposing the sprint plan...`,
|
|
1683
|
+
});
|
|
1684
|
+
this.save();
|
|
1685
|
+
|
|
1686
|
+
try {
|
|
1687
|
+
await this.requirementManager.planWorkflow(
|
|
1688
|
+
requirement, members
|
|
1689
|
+
);
|
|
1690
|
+
} catch (e) {
|
|
1691
|
+
console.error('Sprint workflow decomposition failed:', e.message);
|
|
1692
|
+
}
|
|
1693
|
+
this.save();
|
|
1694
|
+
|
|
1695
|
+
// 3. Execute workflow DAG
|
|
1696
|
+
let summary;
|
|
1697
|
+
try {
|
|
1698
|
+
summary = await this.requirementManager.executeWorkflow(
|
|
1699
|
+
requirement, dept, this.performanceSystem
|
|
1700
|
+
);
|
|
1701
|
+
} catch (e) {
|
|
1702
|
+
console.error('Sprint workflow execution failed:', e.message);
|
|
1703
|
+
requirement.status = 'failed';
|
|
1704
|
+
requirement.completedAt = new Date();
|
|
1705
|
+
requirement.summary = { totalTasks: 0, successTasks: 0, failedTasks: 0, totalDuration: 0, outputs: [], error: e.message };
|
|
1706
|
+
requirement.addGroupMessage(
|
|
1707
|
+
{ name: 'System', role: 'system' },
|
|
1708
|
+
`❌ Sprint requirement execution failed: ${e.message}`,
|
|
1709
|
+
'system', null, { auto: true }
|
|
1710
|
+
);
|
|
1711
|
+
this.save();
|
|
1712
|
+
summary = requirement.summary;
|
|
1713
|
+
}
|
|
1714
|
+
|
|
1715
|
+
// 4. Leader sends report email
|
|
1716
|
+
if (leader && summary) {
|
|
1717
|
+
let reportContent = `Sprint "${title}" completed!\n\n`;
|
|
1718
|
+
reportContent += `📊 Execution Summary:\n`;
|
|
1719
|
+
reportContent += `- Tasks completed: ${summary.successTasks}/${summary.totalTasks}\n`;
|
|
1720
|
+
reportContent += `- Total duration: ${Math.round(summary.totalDuration / 1000)}s\n\n`;
|
|
1721
|
+
reportContent += `📝 Member outputs:\n`;
|
|
1722
|
+
for (const o of (summary.outputs || [])) {
|
|
1723
|
+
reportContent += `\n[${o.agentName} (${o.role})]\n`;
|
|
1724
|
+
reportContent += (o.content || '').slice(0, 300);
|
|
1725
|
+
if ((o.content || '').length > 300) reportContent += '...';
|
|
1726
|
+
reportContent += '\n';
|
|
1727
|
+
}
|
|
1728
|
+
leader.sendMailToBoss(`📋 Sprint Report: ${title}`, reportContent, this);
|
|
1729
|
+
}
|
|
1730
|
+
|
|
1731
|
+
// 5. Update sprint status based on requirement result
|
|
1732
|
+
const { SprintStatus } = await import('@/core/organization/team.js');
|
|
1733
|
+
if (requirement.status === 'completed' || requirement.status === 'pending_approval') {
|
|
1734
|
+
// For pending_approval, the sprint is done but the requirement awaits Boss review
|
|
1735
|
+
sprint.status = SprintStatus.COMPLETED;
|
|
1736
|
+
sprint.completedAt = new Date();
|
|
1737
|
+
sprint.summary = requirement.summary;
|
|
1738
|
+
} else if (requirement.status === 'failed') {
|
|
1739
|
+
sprint.status = SprintStatus.FAILED;
|
|
1740
|
+
sprint.completedAt = new Date();
|
|
1741
|
+
sprint.summary = requirement.summary;
|
|
1742
|
+
}
|
|
1743
|
+
|
|
1744
|
+
this.save();
|
|
1745
|
+
return requirement;
|
|
1746
|
+
}
|
|
1747
|
+
|
|
1748
|
+
/**
|
|
1749
|
+
* Boss sends a message in a requirement's group chat
|
|
1750
|
+
* The leader will see the message and decide whether to adjust the plan
|
|
1751
|
+
* @param {string} requirementId - Requirement ID
|
|
1752
|
+
* @param {string} message - Boss's message
|
|
1753
|
+
* @returns {Promise<object>} Leader's response
|
|
1754
|
+
*/
|
|
1755
|
+
async sendBossGroupMessage(requirementId, message) {
|
|
1756
|
+
const requirement = this.requirementManager.get(requirementId);
|
|
1757
|
+
if (!requirement) throw new Error('Requirement not found');
|
|
1758
|
+
|
|
1759
|
+
const dept = this.findDepartment(requirement.departmentId);
|
|
1760
|
+
if (!dept) throw new Error('Department not found');
|
|
1761
|
+
|
|
1762
|
+
const leader = dept.getLeader() || dept.getMembers()[0];
|
|
1763
|
+
if (!leader) throw new Error('No leader found in department');
|
|
1764
|
+
|
|
1765
|
+
// 1. Add Boss message to group chat (expand [[file:path]] → full format)
|
|
1766
|
+
const { content: expandedMessage, invalidRefs } = expandFileReferences(message, requirement.departmentId, dept.workspacePath);
|
|
1767
|
+
requirement.addGroupMessage(
|
|
1768
|
+
{
|
|
1769
|
+
id: 'boss',
|
|
1770
|
+
name: this.bossName || 'Boss',
|
|
1771
|
+
avatar: this.bossAvatar || null,
|
|
1772
|
+
role: 'Boss',
|
|
1773
|
+
},
|
|
1774
|
+
expandedMessage,
|
|
1775
|
+
'message'
|
|
1776
|
+
);
|
|
1777
|
+
// Auto-feedback: notify about invalid file references
|
|
1778
|
+
if (invalidRefs.length > 0) {
|
|
1779
|
+
const invalidList = invalidRefs.map(f => ` - ${f}`).join('\n');
|
|
1780
|
+
requirement.addGroupMessage(
|
|
1781
|
+
{ id: 'system', name: 'System', role: 'system' },
|
|
1782
|
+
`⚠️ File reference error: the following files do not exist in workspace:\n${invalidList}`,
|
|
1783
|
+
'message', null, { auto: true }
|
|
1784
|
+
);
|
|
1785
|
+
}
|
|
1786
|
+
this.save();
|
|
1787
|
+
|
|
1788
|
+
// 1.5 Trigger group chat loop: notify all members of new message (Boss message, everyone should pay attention)
|
|
1789
|
+
const allMembers = dept.getMembers();
|
|
1790
|
+
for (const member of allMembers) {
|
|
1791
|
+
// Delayed trigger to avoid everyone processing at the same time
|
|
1792
|
+
setTimeout(() => {
|
|
1793
|
+
groupChatLoop.triggerImmediate(member.id, requirementId, { from: { id: 'boss' }, content: message }).catch(() => {});
|
|
1794
|
+
}, 500 + Math.random() * 2000);
|
|
1795
|
+
}
|
|
1796
|
+
|
|
1797
|
+
// 2. Leader asynchronously handles Boss message (use LLM to decide if adjustment needed)
|
|
1798
|
+
const leaderResponse = await this._leaderHandleBossMessage(leader, requirement, dept, message);
|
|
1799
|
+
|
|
1800
|
+
// 3. Add Leader reply to group chat
|
|
1801
|
+
if (leaderResponse.reply) {
|
|
1802
|
+
requirement.addGroupMessage(leader, leaderResponse.reply, 'message', null, { auto: true });
|
|
1803
|
+
}
|
|
1804
|
+
|
|
1805
|
+
// 4. Execute operations based on Leader decision
|
|
1806
|
+
if (leaderResponse.action === 'stop') {
|
|
1807
|
+
// Stop project
|
|
1808
|
+
requirement.status = 'failed';
|
|
1809
|
+
requirement.completedAt = new Date();
|
|
1810
|
+
requirement.updateLiveStatus({
|
|
1811
|
+
currentAction: 'Boss requested stop, project halted',
|
|
1812
|
+
currentAgent: null,
|
|
1813
|
+
});
|
|
1814
|
+
requirement.addGroupMessage(
|
|
1815
|
+
{ name: 'System', role: 'system' },
|
|
1816
|
+
`⏹️ Project stopped by Boss request`,
|
|
1817
|
+
'system', null, { auto: true }
|
|
1818
|
+
);
|
|
1819
|
+
} else if (leaderResponse.action === 'restart') {
|
|
1820
|
+
// Restart project (mark as failed, frontend can use restart button)
|
|
1821
|
+
requirement.addGroupMessage(
|
|
1822
|
+
{ name: 'System', role: 'system' },
|
|
1823
|
+
`🔄 Boss requested project restart, replanning...`,
|
|
1824
|
+
'system', null, { auto: true }
|
|
1825
|
+
);
|
|
1826
|
+
// Async re-execute, non-blocking
|
|
1827
|
+
const title = requirement.title;
|
|
1828
|
+
const description = requirement.description;
|
|
1829
|
+
const deptId = requirement.departmentId;
|
|
1830
|
+
// Delete old requirement
|
|
1831
|
+
this.requirementManager.requirements.delete(requirementId);
|
|
1832
|
+
// Re-dispatch
|
|
1833
|
+
this.assignTaskToDepartment(deptId, description, title).catch(e => {
|
|
1834
|
+
console.error('Restart requirement failed:', e.message);
|
|
1835
|
+
});
|
|
1836
|
+
} else if (leaderResponse.action === 'adjust' && leaderResponse.adjustments) {
|
|
1837
|
+
// Adjust plan: leader has explained the adjustment in reply, trigger actual workflow modification
|
|
1838
|
+
requirement.addGroupMessage(
|
|
1839
|
+
{ name: 'System', role: 'system' },
|
|
1840
|
+
`📝 ${leader.name} is adjusting the plan based on Boss instructions...`,
|
|
1841
|
+
'system'
|
|
1842
|
+
);
|
|
1843
|
+
|
|
1844
|
+
// Record old workflow for reference
|
|
1845
|
+
const previousWorkflow = requirement.workflow?.nodes?.map(n =>
|
|
1846
|
+
`- [${n.status}] ${n.title} → ${n.assigneeName || 'unknown'}${n.dependencies?.length ? ` (deps: ${n.dependencies.join(', ')})` : ''}`
|
|
1847
|
+
).join('\n') || 'No previous workflow';
|
|
1848
|
+
|
|
1849
|
+
// Record existing output files (preserved during adjustment, not deleted)
|
|
1850
|
+
const existingOutputs = requirement.outputs || [];
|
|
1851
|
+
|
|
1852
|
+
// Reset requirement status (preserve outputs, do not delete existing files)
|
|
1853
|
+
requirement.status = RequirementStatus.PLANNING;
|
|
1854
|
+
requirement.workflow = null;
|
|
1855
|
+
// Note: do not clear outputs, modify/supplement on top of existing results
|
|
1856
|
+
requirement.summary = null;
|
|
1857
|
+
requirement.completedAt = null;
|
|
1858
|
+
requirement.updateLiveStatus({
|
|
1859
|
+
currentAgent: leader.name,
|
|
1860
|
+
currentAgentId: leader.id,
|
|
1861
|
+
currentAgentAvatar: leader.avatar,
|
|
1862
|
+
currentAction: `${leader.name} is re-planning the workflow based on Boss's instructions...`,
|
|
1863
|
+
toolCallsInProgress: [],
|
|
1864
|
+
recentFileChanges: [],
|
|
1865
|
+
});
|
|
1866
|
+
this.save();
|
|
1867
|
+
|
|
1868
|
+
// Async replan + execute (non-blocking API return)
|
|
1869
|
+
const members = dept.getMembers();
|
|
1870
|
+
const adjustmentContext = {
|
|
1871
|
+
bossMessage: message,
|
|
1872
|
+
adjustments: leaderResponse.adjustments,
|
|
1873
|
+
previousWorkflow,
|
|
1874
|
+
existingOutputs: existingOutputs.map(o => o.fileName || o.title || 'unknown').join(', '),
|
|
1875
|
+
};
|
|
1876
|
+
|
|
1877
|
+
(async () => {
|
|
1878
|
+
try {
|
|
1879
|
+
await this.requirementManager.planWorkflow(
|
|
1880
|
+
requirement, members, adjustmentContext
|
|
1881
|
+
);
|
|
1882
|
+
this.save();
|
|
1883
|
+
|
|
1884
|
+
// Re-execute adjusted workflow
|
|
1885
|
+
await this.requirementManager.executeWorkflow(
|
|
1886
|
+
requirement, dept, this.performanceSystem
|
|
1887
|
+
);
|
|
1888
|
+
this.save();
|
|
1889
|
+
|
|
1890
|
+
// After completion, leader sends report email
|
|
1891
|
+
if (leader) {
|
|
1892
|
+
const summary = requirement.summary || {};
|
|
1893
|
+
let reportContent = `Requirement "${requirement.title}" has been re-completed after adjustment!\n\n`;
|
|
1894
|
+
reportContent += `📊 Execution Results:\n`;
|
|
1895
|
+
reportContent += `- Completed tasks: ${summary.successTasks || 0}/${summary.totalTasks || 0}\n`;
|
|
1896
|
+
reportContent += `- Total duration: ${Math.round((summary.totalDuration || 0) / 1000)}s\n\n`;
|
|
1897
|
+
reportContent += `📝 Adjustment reason: ${message}\n`;
|
|
1898
|
+
leader.sendMailToBoss(`📋 Adjusted Requirement Report: ${requirement.title}`, reportContent, this);
|
|
1899
|
+
}
|
|
1900
|
+
} catch (e) {
|
|
1901
|
+
console.error('Adjust workflow failed:', e.message);
|
|
1902
|
+
requirement.status = RequirementStatus.FAILED;
|
|
1903
|
+
requirement.completedAt = new Date();
|
|
1904
|
+
requirement.addGroupMessage(
|
|
1905
|
+
{ name: 'System', role: 'system' },
|
|
1906
|
+
`❌ Adjustment plan execution failed: ${e.message}`,
|
|
1907
|
+
'system', null, { auto: true }
|
|
1908
|
+
);
|
|
1909
|
+
this.save();
|
|
1910
|
+
}
|
|
1911
|
+
})();
|
|
1912
|
+
} else if (leaderResponse.action === 'approve') {
|
|
1913
|
+
// Boss approved the requirement — finalize it
|
|
1914
|
+
requirement.status = RequirementStatus.COMPLETED;
|
|
1915
|
+
requirement.completedAt = new Date();
|
|
1916
|
+
requirement.updateLiveStatus({
|
|
1917
|
+
currentAction: 'Boss approved — requirement completed',
|
|
1918
|
+
currentAgent: null,
|
|
1919
|
+
});
|
|
1920
|
+
requirement.addGroupMessage(
|
|
1921
|
+
{ name: 'System', role: 'system' },
|
|
1922
|
+
`✅ Requirement "${requirement.title}" has been approved by Boss and is now completed!`,
|
|
1923
|
+
'system', null, { auto: true }
|
|
1924
|
+
);
|
|
1925
|
+
|
|
1926
|
+
// Leader sends completion report email
|
|
1927
|
+
if (leader) {
|
|
1928
|
+
const summary = requirement.summary || {};
|
|
1929
|
+
let reportContent = `Requirement "${requirement.title}" has been approved and completed!\n\n`;
|
|
1930
|
+
reportContent += `📊 Execution Results:\n`;
|
|
1931
|
+
reportContent += `- Completed tasks: ${summary.successTasks || 0}/${summary.totalTasks || 0}\n`;
|
|
1932
|
+
reportContent += `- Total duration: ${Math.round((summary.totalDuration || 0) / 1000)}s\n`;
|
|
1933
|
+
leader.sendMailToBoss(`✅ Requirement Approved: ${requirement.title}`, reportContent, this);
|
|
1934
|
+
}
|
|
1935
|
+
}
|
|
1936
|
+
// action === 'continue' → No special handling needed, continue as normal
|
|
1937
|
+
|
|
1938
|
+
this.save();
|
|
1939
|
+
|
|
1940
|
+
return {
|
|
1941
|
+
requirementId,
|
|
1942
|
+
bossMessage: message,
|
|
1943
|
+
leaderReply: leaderResponse.reply,
|
|
1944
|
+
action: leaderResponse.action,
|
|
1945
|
+
};
|
|
1946
|
+
}
|
|
1947
|
+
|
|
1948
|
+
/**
|
|
1949
|
+
* Boss sends a message in department group chat
|
|
1950
|
+
* @param {string} departmentId - Department ID
|
|
1951
|
+
* @param {string} message - Message content
|
|
1952
|
+
*/
|
|
1953
|
+
sendBossDeptGroupMessage(departmentId, message) {
|
|
1954
|
+
const dept = this.findDepartment(departmentId);
|
|
1955
|
+
if (!dept) throw new Error('Department not found');
|
|
1956
|
+
|
|
1957
|
+
// 1. Add Boss message to department group chat
|
|
1958
|
+
dept.addGroupMessage(
|
|
1959
|
+
{
|
|
1960
|
+
id: 'boss',
|
|
1961
|
+
name: this.bossName || 'Boss',
|
|
1962
|
+
avatar: this.bossAvatar || null,
|
|
1963
|
+
role: 'Boss',
|
|
1964
|
+
},
|
|
1965
|
+
message,
|
|
1966
|
+
'message'
|
|
1967
|
+
);
|
|
1968
|
+
this.save();
|
|
1969
|
+
|
|
1970
|
+
// 2. Trigger group chat loop: notify all department members of new message
|
|
1971
|
+
const allMembers = dept.getMembers();
|
|
1972
|
+
for (const member of allMembers) {
|
|
1973
|
+
// Delayed trigger to avoid everyone processing at the same time
|
|
1974
|
+
setTimeout(() => {
|
|
1975
|
+
groupChatLoop.triggerImmediate(member.id, `dept-${dept.id}`, { from: { id: 'boss' }, content: message }).catch(() => {});
|
|
1976
|
+
}, 500 + Math.random() * 2000);
|
|
1977
|
+
}
|
|
1978
|
+
|
|
1979
|
+
return { groupChat: dept.groupChat };
|
|
1980
|
+
}
|
|
1981
|
+
|
|
1982
|
+
/**
|
|
1983
|
+
* Leader uses LLM to handle Boss message in group chat
|
|
1984
|
+
* @private
|
|
1985
|
+
*/
|
|
1986
|
+
async _leaderHandleBossMessage(leader, requirement, department, bossMessage) {
|
|
1987
|
+
if (!leader.canChat()) {
|
|
1988
|
+
return {
|
|
1989
|
+
reply: `Received Boss instructions! I will execute them diligently.`,
|
|
1990
|
+
action: 'continue',
|
|
1991
|
+
};
|
|
1992
|
+
}
|
|
1993
|
+
|
|
1994
|
+
try {
|
|
1995
|
+
// Build group chat history context (latest 20 messages)
|
|
1996
|
+
const recentChat = (requirement.groupChat || []).slice(-20).map(m => {
|
|
1997
|
+
const sender = m.from?.name || 'Unknown';
|
|
1998
|
+
const role = m.from?.role ? `(${m.from.role})` : '';
|
|
1999
|
+
return `[${sender}${role}]: ${m.content}`;
|
|
2000
|
+
}).join('\n');
|
|
2001
|
+
|
|
2002
|
+
// Build workflow status
|
|
2003
|
+
const workflowStatus = requirement.workflow?.nodes?.map(n => {
|
|
2004
|
+
const agent = department.agents.get(n.assigneeId);
|
|
2005
|
+
return `- [${n.status}] ${n.title} (assigned to: ${agent?.name || n.assigneeName || 'unknown'})`;
|
|
2006
|
+
}).join('\n') || 'No workflow yet';
|
|
2007
|
+
|
|
2008
|
+
const p = leader.personality || {};
|
|
2009
|
+
const response = await leader.chat([
|
|
2010
|
+
{
|
|
2011
|
+
role: 'system',
|
|
2012
|
+
content: `You are "${leader.name}", the project leader of department "${department.name}".
|
|
2013
|
+
Your personality: ${p.trait || 'Professional'}. Speaking style: ${p.tone || 'Normal'}.
|
|
2014
|
+
You are leading the team to work on requirement "${requirement.title}": ${requirement.description}
|
|
2015
|
+
|
|
2016
|
+
Current project status: ${requirement.status}
|
|
2017
|
+
Current workflow:
|
|
2018
|
+
${workflowStatus}
|
|
2019
|
+
|
|
2020
|
+
Recent group chat:
|
|
2021
|
+
${recentChat}
|
|
2022
|
+
|
|
2023
|
+
The Boss (your employer) just sent a message in the group chat. You need to:
|
|
2024
|
+
1. Carefully analyze the Boss's intent
|
|
2025
|
+
2. Decide what action to take
|
|
2026
|
+
3. Reply naturally in your personality style, addressing the Boss respectfully
|
|
2027
|
+
|
|
2028
|
+
You MUST reply in JSON format:
|
|
2029
|
+
{
|
|
2030
|
+
"reply": "Your natural reply to the Boss (addressing them as Boss, speaking in your personality style, explaining what you'll do)",
|
|
2031
|
+
"action": "continue|adjust|stop|restart|approve",
|
|
2032
|
+
"adjustments": "If action is 'adjust', briefly describe what changes you'll make to the plan"
|
|
2033
|
+
}
|
|
2034
|
+
|
|
2035
|
+
Action rules:
|
|
2036
|
+
- "continue": Boss is just commenting/encouraging, no changes needed. Or giving minor feedback that doesn't change the plan.
|
|
2037
|
+
- "approve": Boss is satisfied with the results and wants to close/accept/finalize the requirement. Use when Boss says things like "looks good", "approved", "OK", "done", "accept", "通过", "可以", "没问题", "完成", "好的", "确认", "LGTM", "ship it", etc. ${requirement.status === 'pending_approval' ? '**IMPORTANT: The project is currently PENDING APPROVAL. If the Boss seems satisfied or gives positive feedback, use "approve".**' : ''}
|
|
2038
|
+
- "adjust": Boss wants to modify, supplement, or revise the current plan. IMPORTANT: This means working on top of existing results — existing files and outputs will be PRESERVED, only new/modified content will be added. Use this when Boss says things like "add more", "revise", "change X to Y", "also include", "supplement", "modify", "adjust" etc.
|
|
2039
|
+
- "stop": Boss explicitly says to stop, halt, or cancel the project.
|
|
2040
|
+
- "restart": Boss EXPLICITLY wants to start over completely from scratch, redo everything, or completely restart. Use ONLY when Boss clearly says "start over", "redo from scratch", "start fresh" etc. All existing files will be DELETED.
|
|
2041
|
+
|
|
2042
|
+
Reply in the same language the Boss used. Be concise but warm.`
|
|
2043
|
+
},
|
|
2044
|
+
{
|
|
2045
|
+
role: 'user',
|
|
2046
|
+
content: `Boss says: "${bossMessage}"`
|
|
2047
|
+
},
|
|
2048
|
+
], { temperature: 0.7, maxTokens: 512 });
|
|
2049
|
+
|
|
2050
|
+
// Parse JSON
|
|
2051
|
+
const tick = String.fromCharCode(96);
|
|
2052
|
+
const fence = tick + tick + tick;
|
|
2053
|
+
let jsonStr = response.content
|
|
2054
|
+
.replace(fence + 'json', '').replace(fence, '')
|
|
2055
|
+
.replace(fence + 'json', '').replace(fence, '')
|
|
2056
|
+
.trim();
|
|
2057
|
+
|
|
2058
|
+
try {
|
|
2059
|
+
const parsed = JSON.parse(jsonStr);
|
|
2060
|
+
return {
|
|
2061
|
+
reply: parsed.reply || 'Understood, Boss!',
|
|
2062
|
+
action: ['continue', 'adjust', 'stop', 'restart', 'approve'].includes(parsed.action) ? parsed.action : 'continue',
|
|
2063
|
+
adjustments: parsed.adjustments || null,
|
|
2064
|
+
};
|
|
2065
|
+
} catch {
|
|
2066
|
+
// JSON parse failed, use raw reply
|
|
2067
|
+
return {
|
|
2068
|
+
reply: response.content?.trim() || 'Understood, Boss!',
|
|
2069
|
+
action: 'continue',
|
|
2070
|
+
};
|
|
2071
|
+
}
|
|
2072
|
+
} catch (e) {
|
|
2073
|
+
console.error(`Leader ${leader.name} failed to handle boss message:`, e.message);
|
|
2074
|
+
return {
|
|
2075
|
+
reply: `Received Boss instructions, I will handle it promptly!`,
|
|
2076
|
+
action: 'continue',
|
|
2077
|
+
};
|
|
2078
|
+
}
|
|
2079
|
+
}
|
|
2080
|
+
|
|
2081
|
+
/**
|
|
2082
|
+
* Get full company state data (for Web rendering)
|
|
2083
|
+
*/
|
|
2084
|
+
getFullState() {
|
|
2085
|
+
const departments = [];
|
|
2086
|
+
|
|
2087
|
+
// Calculate company-wide Token/cost statistics
|
|
2088
|
+
let companyTotalTokens = 0;
|
|
2089
|
+
let companyTotalCost = 0;
|
|
2090
|
+
|
|
2091
|
+
this.departments.forEach(dept => {
|
|
2092
|
+
let deptTokens = 0;
|
|
2093
|
+
let deptCost = 0;
|
|
2094
|
+
const members = dept.getMembers().map(a => {
|
|
2095
|
+
const usage = a.tokenUsage || { totalTokens: 0, totalCost: 0, promptTokens: 0, completionTokens: 0, callCount: 0 };
|
|
2096
|
+
deptTokens += usage.totalTokens;
|
|
2097
|
+
deptCost += usage.totalCost;
|
|
2098
|
+
return {
|
|
2099
|
+
id: a.id,
|
|
2100
|
+
name: a.name,
|
|
2101
|
+
role: a.role,
|
|
2102
|
+
avatar: a.avatar,
|
|
2103
|
+
gender: a.gender,
|
|
2104
|
+
age: a.age,
|
|
2105
|
+
signature: a.signature,
|
|
2106
|
+
personality: a.personality,
|
|
2107
|
+
status: a.status,
|
|
2108
|
+
provider: a.getProviderDisplayInfo(),
|
|
2109
|
+
cliBackend: a.cliBackend || null,
|
|
2110
|
+
fallbackProvider: a.getFallbackProviderName(),
|
|
2111
|
+
skills: a.skills,
|
|
2112
|
+
reportsTo: a.reportsTo,
|
|
2113
|
+
subordinates: a.subordinates,
|
|
2114
|
+
memory: a.memory.getSummary(),
|
|
2115
|
+
performanceHistory: a.performanceHistory,
|
|
2116
|
+
avgScore: a.performanceHistory.length > 0
|
|
2117
|
+
? Math.round(a.performanceHistory.reduce((s, p) => s + p.score, 0) / a.performanceHistory.length)
|
|
2118
|
+
: null,
|
|
2119
|
+
taskCount: a.taskHistory.length,
|
|
2120
|
+
tokenUsage: { ...usage },
|
|
2121
|
+
};
|
|
2122
|
+
});
|
|
2123
|
+
|
|
2124
|
+
companyTotalTokens += deptTokens;
|
|
2125
|
+
companyTotalCost += deptCost;
|
|
2126
|
+
|
|
2127
|
+
departments.push({
|
|
2128
|
+
id: dept.id,
|
|
2129
|
+
name: dept.name,
|
|
2130
|
+
mission: dept.mission,
|
|
2131
|
+
status: dept.status,
|
|
2132
|
+
leader: dept.leader,
|
|
2133
|
+
workspacePath: dept.workspacePath || null,
|
|
2134
|
+
groupChat: dept.groupChat || [],
|
|
2135
|
+
members,
|
|
2136
|
+
tokenUsage: { totalTokens: deptTokens, totalCost: deptCost },
|
|
2137
|
+
});
|
|
2138
|
+
});
|
|
2139
|
+
|
|
2140
|
+
// Add secretary and HR consumption
|
|
2141
|
+
const secUsage = this.secretary.tokenUsage || { totalTokens: 0, totalCost: 0 };
|
|
2142
|
+
const hrUsage = this.secretary.hrAssistant.employee.tokenUsage || { totalTokens: 0, totalCost: 0 };
|
|
2143
|
+
companyTotalTokens += secUsage.totalTokens + hrUsage.totalTokens;
|
|
2144
|
+
companyTotalCost += secUsage.totalCost + hrUsage.totalCost;
|
|
2145
|
+
|
|
2146
|
+
return {
|
|
2147
|
+
id: this.id,
|
|
2148
|
+
name: this.name,
|
|
2149
|
+
boss: this.bossName,
|
|
2150
|
+
bossAvatar: this.bossAvatar,
|
|
2151
|
+
secretary: {
|
|
2152
|
+
name: this.secretary.name,
|
|
2153
|
+
avatar: this.secretary.avatar,
|
|
2154
|
+
gender: this.secretary.gender,
|
|
2155
|
+
age: this.secretary.age,
|
|
2156
|
+
signature: this.secretary.signature,
|
|
2157
|
+
prompt: this.secretary.prompt,
|
|
2158
|
+
provider: this.secretary.getProviderDisplayInfo().name,
|
|
2159
|
+
providerId: this.secretary.getProviderDisplayInfo().id,
|
|
2160
|
+
// Optional general-purpose + CLI + browser provider list (enabled only)
|
|
2161
|
+
availableProviders: [
|
|
2162
|
+
...this.providerRegistry.getByCategory('general').filter(p => !p.isWeb).map(p => ({
|
|
2163
|
+
id: p.id,
|
|
2164
|
+
name: p.name,
|
|
2165
|
+
})),
|
|
2166
|
+
...this.providerRegistry.getByCategory('cli').map(p => ({
|
|
2167
|
+
id: p.id,
|
|
2168
|
+
name: `${p.cliIcon || '🖥️'} ${p.name} (CLI)`,
|
|
2169
|
+
isCLI: true,
|
|
2170
|
+
cliBackendId: p.cliBackendId,
|
|
2171
|
+
})),
|
|
2172
|
+
...this.providerRegistry.getByCategory('browser').map(p => ({
|
|
2173
|
+
id: p.id,
|
|
2174
|
+
name: `🌐 ${p.name}`,
|
|
2175
|
+
isWeb: true,
|
|
2176
|
+
})),
|
|
2177
|
+
],
|
|
2178
|
+
hrAssistant: {
|
|
2179
|
+
name: this.secretary.hrAssistant.employee.name,
|
|
2180
|
+
avatar: this.secretary.hrAssistant.employee.avatar,
|
|
2181
|
+
signature: this.secretary.hrAssistant.employee.signature,
|
|
2182
|
+
},
|
|
2183
|
+
},
|
|
2184
|
+
departments,
|
|
2185
|
+
budget: {
|
|
2186
|
+
totalTokens: companyTotalTokens,
|
|
2187
|
+
totalCost: Math.round(companyTotalCost * 10000) / 10000,
|
|
2188
|
+
secretaryUsage: { ...secUsage },
|
|
2189
|
+
hrUsage: { ...hrUsage },
|
|
2190
|
+
},
|
|
2191
|
+
mailbox: this.mailbox.slice(-50).map(m => ({
|
|
2192
|
+
...m,
|
|
2193
|
+
from: { ...m.from },
|
|
2194
|
+
replies: (m.replies || []).slice(-10),
|
|
2195
|
+
})),
|
|
2196
|
+
unreadMailCount: this.mailbox.filter(m => !m.read).length,
|
|
2197
|
+
pendingPlans: [...this.pendingPlans.entries()].map(([id, p]) => {
|
|
2198
|
+
if (p.type === 'adjustment') {
|
|
2199
|
+
return {
|
|
2200
|
+
planId: id,
|
|
2201
|
+
type: 'adjustment',
|
|
2202
|
+
departmentId: p.departmentId,
|
|
2203
|
+
departmentName: p.departmentName,
|
|
2204
|
+
adjustGoal: p.adjustGoal,
|
|
2205
|
+
reasoning: p.adjustPlan?.reasoning,
|
|
2206
|
+
fires: p.adjustPlan?.fires || [],
|
|
2207
|
+
hires: p.adjustPlan?.hires || [],
|
|
2208
|
+
};
|
|
2209
|
+
}
|
|
2210
|
+
return {
|
|
2211
|
+
planId: id,
|
|
2212
|
+
type: 'recruitment',
|
|
2213
|
+
name: p.name,
|
|
2214
|
+
mission: p.mission,
|
|
2215
|
+
members: (p.teamPlan?.members || []).map(m => ({
|
|
2216
|
+
templateId: m.templateId,
|
|
2217
|
+
title: m.templateTitle,
|
|
2218
|
+
name: m.name,
|
|
2219
|
+
isLeader: m.isLeader,
|
|
2220
|
+
reportsTo: m.reportsTo !== null ? p.teamPlan.members[m.reportsTo]?.name : null,
|
|
2221
|
+
})),
|
|
2222
|
+
};
|
|
2223
|
+
}),
|
|
2224
|
+
chatHistory: this.chatHistory.slice(-30),
|
|
2225
|
+
progressReports: this.progressReports.slice(-20),
|
|
2226
|
+
talentMarket: this.talentMarket.listAvailable().map(p => ({
|
|
2227
|
+
id: p.id,
|
|
2228
|
+
name: p.name,
|
|
2229
|
+
role: p.role,
|
|
2230
|
+
avatar: p.avatar,
|
|
2231
|
+
gender: p.gender,
|
|
2232
|
+
age: p.age,
|
|
2233
|
+
skills: [...p.skills, ...p.acquiredSkills],
|
|
2234
|
+
dismissalReason: p.dismissalReason,
|
|
2235
|
+
performanceScore: p.performanceData?.averageScore,
|
|
2236
|
+
registeredAt: p.registeredAt,
|
|
2237
|
+
memoryCount: p.memorySnapshot ? (p.memorySnapshot.shortTerm?.length || 0) + (p.memorySnapshot.longTerm?.length || 0) : 0,
|
|
2238
|
+
})),
|
|
2239
|
+
providerDashboard: this.providerRegistry.getStats(),
|
|
2240
|
+
messageBusStats: this.messageBus.getStats(),
|
|
2241
|
+
// Boss-Agent private chat session list
|
|
2242
|
+
agentChatSessions: this._getAgentChatSessions(),
|
|
2243
|
+
requirements: this.requirementManager.listAll().map(r => r.serialize()),
|
|
2244
|
+
teams: this.teamManager.listAll().map(t => t.serialize()),
|
|
2245
|
+
logs: this.logs.slice(-50),
|
|
2246
|
+
};
|
|
2247
|
+
}
|
|
2248
|
+
|
|
2249
|
+
/**
|
|
2250
|
+
* Get recent messages from message bus
|
|
2251
|
+
*/
|
|
2252
|
+
getRecentMessages(limit = 20) {
|
|
2253
|
+
return this.messageBus.getRecent(limit);
|
|
2254
|
+
}
|
|
2255
|
+
|
|
2256
|
+
/**
|
|
2257
|
+
* Get conversation between Agents
|
|
2258
|
+
*/
|
|
2259
|
+
getConversation(agentId1, agentId2, limit = 50) {
|
|
2260
|
+
return this.messageBus.getConversation(agentId1, agentId2, limit);
|
|
2261
|
+
}
|
|
2262
|
+
|
|
2263
|
+
/**
|
|
2264
|
+
* Get workspace file tree
|
|
2265
|
+
*/
|
|
2266
|
+
async getWorkspaceFiles(departmentId) {
|
|
2267
|
+
const dept = this.findDepartment(departmentId);
|
|
2268
|
+
if (!dept || !dept.workspacePath) return [];
|
|
2269
|
+
return this.workspaceManager.getFileTree(dept.workspacePath);
|
|
2270
|
+
}
|
|
2271
|
+
|
|
2272
|
+
/**
|
|
2273
|
+
* Get shallow (one-level) workspace file listing
|
|
2274
|
+
*/
|
|
2275
|
+
async getShallowWorkspaceFiles(departmentId, subPath = '') {
|
|
2276
|
+
const dept = this.findDepartment(departmentId);
|
|
2277
|
+
if (!dept || !dept.workspacePath) return [];
|
|
2278
|
+
return this.workspaceManager.getShallowFileTree(dept.workspacePath, subPath);
|
|
2279
|
+
}
|
|
2280
|
+
|
|
2281
|
+
/**
|
|
2282
|
+
* Read workspace file
|
|
2283
|
+
*/
|
|
2284
|
+
async readWorkspaceFile(departmentId, filePath) {
|
|
2285
|
+
const dept = this.findDepartment(departmentId);
|
|
2286
|
+
if (!dept || !dept.workspacePath) throw new Error('Department workspace does not exist');
|
|
2287
|
+
return this.workspaceManager.readFile(dept.workspacePath, filePath);
|
|
2288
|
+
}
|
|
2289
|
+
|
|
2290
|
+
printCompanyOverview() {
|
|
2291
|
+
console.log(`\n${'='.repeat(60)}`);
|
|
2292
|
+
console.log(`🏢 "${this.name}" Company Overview`);
|
|
2293
|
+
console.log(`${'='.repeat(60)}`);
|
|
2294
|
+
console.log(`👤 Boss: ${this.bossName}`);
|
|
2295
|
+
console.log(`🏢 Departments: ${this.departments.size}`);
|
|
2296
|
+
|
|
2297
|
+
this.departments.forEach(dept => {
|
|
2298
|
+
console.log(`\n 📁 ${dept.name} (${dept.status})`);
|
|
2299
|
+
console.log(` Mission: ${dept.mission}`);
|
|
2300
|
+
console.log(` Members: ${dept.agents.size}`);
|
|
2301
|
+
dept.printOrgChart();
|
|
2302
|
+
});
|
|
2303
|
+
|
|
2304
|
+
this.talentMarket.print();
|
|
2305
|
+
console.log(`${'='.repeat(60)}\n`);
|
|
2306
|
+
}
|
|
2307
|
+
|
|
2308
|
+
// ========== Persistence Serialization ==========
|
|
2309
|
+
|
|
2310
|
+
/**
|
|
2311
|
+
* Serialize complete company state (for disk persistence)
|
|
2312
|
+
*/
|
|
2313
|
+
serialize() {
|
|
2314
|
+
// Serialize departments and Agents
|
|
2315
|
+
const departments = [];
|
|
2316
|
+
this.departments.forEach(dept => {
|
|
2317
|
+
const members = dept.getMembers().map(a => a.serialize());
|
|
2318
|
+
departments.push({
|
|
2319
|
+
id: dept.id,
|
|
2320
|
+
name: dept.name,
|
|
2321
|
+
mission: dept.mission,
|
|
2322
|
+
status: dept.status,
|
|
2323
|
+
leader: dept.leader,
|
|
2324
|
+
workspacePath: dept.workspacePath,
|
|
2325
|
+
createdAt: dept.createdAt,
|
|
2326
|
+
// groupChat is persisted in chatStore files (data/chats/group-dept-{id}/)
|
|
2327
|
+
members,
|
|
2328
|
+
});
|
|
2329
|
+
});
|
|
2330
|
+
|
|
2331
|
+
// Serialize provider configs (only save API Key/session and enabled state)
|
|
2332
|
+
const providerConfigs = {};
|
|
2333
|
+
this.providerRegistry.listAll().forEach(p => {
|
|
2334
|
+
if (p.apiKey || p.cookie || p.enabled) {
|
|
2335
|
+
providerConfigs[p.id] = { apiKey: p.apiKey, enabled: p.enabled };
|
|
2336
|
+
if (p.isWeb && p.cookie) {
|
|
2337
|
+
providerConfigs[p.id].cookie = p.cookie;
|
|
2338
|
+
}
|
|
2339
|
+
}
|
|
2340
|
+
});
|
|
2341
|
+
|
|
2342
|
+
// Serialize talent market
|
|
2343
|
+
const talentPool = [];
|
|
2344
|
+
this.talentMarket.pool.forEach((profile, id) => {
|
|
2345
|
+
talentPool.push({
|
|
2346
|
+
...profile,
|
|
2347
|
+
// Only save provider id
|
|
2348
|
+
provider: profile.provider ? { id: profile.provider.id } : null,
|
|
2349
|
+
});
|
|
2350
|
+
});
|
|
2351
|
+
|
|
2352
|
+
return {
|
|
2353
|
+
_version: 1,
|
|
2354
|
+
id: this.id,
|
|
2355
|
+
name: this.name,
|
|
2356
|
+
bossName: this.bossName,
|
|
2357
|
+
bossAvatar: this.bossAvatar,
|
|
2358
|
+
departments,
|
|
2359
|
+
providerConfigs,
|
|
2360
|
+
talentPool,
|
|
2361
|
+
mailbox: this.mailbox.slice(-200),
|
|
2362
|
+
chatSessionId: this.chatSessionId,
|
|
2363
|
+
chatHistory: this.chatHistory.slice(-50),
|
|
2364
|
+
progressReports: this.progressReports.slice(-30),
|
|
2365
|
+
logs: this.logs.slice(-100),
|
|
2366
|
+
secretary: {
|
|
2367
|
+
name: this.secretary.name,
|
|
2368
|
+
avatar: this.secretary.avatar,
|
|
2369
|
+
signature: this.secretary.signature,
|
|
2370
|
+
prompt: this.secretary.prompt,
|
|
2371
|
+
providerId: this.secretary.getProviderDisplayInfo().id,
|
|
2372
|
+
cliBackend: this.secretary.cliBackend || null,
|
|
2373
|
+
tokenUsage: { ...this.secretary.tokenUsage },
|
|
2374
|
+
hrTokenUsage: { ...this.secretary.hrAssistant.employee.tokenUsage },
|
|
2375
|
+
},
|
|
2376
|
+
messageBusMessages: this.messageBus.messages.slice(-500).map(m => m.toJSON()),
|
|
2377
|
+
requirements: this.requirementManager.serialize(),
|
|
2378
|
+
teams: this.teamManager.serialize(),
|
|
2379
|
+
cronJobs: cronScheduler.serialize(),
|
|
2380
|
+
cliBackends: cliBackendRegistry.serialize(),
|
|
2381
|
+
savedAt: new Date(),
|
|
2382
|
+
};
|
|
2383
|
+
}
|
|
2384
|
+
|
|
2385
|
+
/**
|
|
2386
|
+
* Restore company from serialized data (static factory method)
|
|
2387
|
+
*/
|
|
2388
|
+
static deserialize(data) {
|
|
2389
|
+
if (!data || !data.name) throw new Error('Invalid company state data');
|
|
2390
|
+
|
|
2391
|
+
// Create shell company (don't trigger full initialization)
|
|
2392
|
+
// Get the real apiKey for the secretary's provider from providerConfigs
|
|
2393
|
+
const secretaryProviderId = data.secretary?.providerId || 'deepseek-v3';
|
|
2394
|
+
const secretaryApiKey = data.providerConfigs?.[secretaryProviderId]?.apiKey || 'sk-restored';
|
|
2395
|
+
const company = new Company(data.name, data.bossName, {
|
|
2396
|
+
providerId: secretaryProviderId,
|
|
2397
|
+
apiKey: secretaryApiKey,
|
|
2398
|
+
secretaryName: data.secretary?.name,
|
|
2399
|
+
secretaryAvatar: data.secretary?.avatar,
|
|
2400
|
+
});
|
|
2401
|
+
|
|
2402
|
+
// Restore ID
|
|
2403
|
+
company.id = data.id;
|
|
2404
|
+
|
|
2405
|
+
// Restore boss avatar
|
|
2406
|
+
if (data.bossAvatar) {
|
|
2407
|
+
company.bossAvatar = data.bossAvatar;
|
|
2408
|
+
}
|
|
2409
|
+
|
|
2410
|
+
// Restore provider configs
|
|
2411
|
+
if (data.providerConfigs) {
|
|
2412
|
+
for (const [pid, cfg] of Object.entries(data.providerConfigs)) {
|
|
2413
|
+
try {
|
|
2414
|
+
// For web providers, restore session and configure webClientRegistry
|
|
2415
|
+
if (cfg.cookie) {
|
|
2416
|
+
const provider = company.providerRegistry.getById(pid);
|
|
2417
|
+
if (provider && provider.isWeb) {
|
|
2418
|
+
provider.cookie = cfg.cookie;
|
|
2419
|
+
provider.enabled = cfg.enabled;
|
|
2420
|
+
webClientRegistry.configureCookie(pid, cfg.cookie);
|
|
2421
|
+
continue;
|
|
2422
|
+
}
|
|
2423
|
+
}
|
|
2424
|
+
company.providerRegistry.configure(pid, cfg.apiKey);
|
|
2425
|
+
// Clear old LLM client cache to ensure the restored real apiKey is used
|
|
2426
|
+
llmClient.clearClient(pid);
|
|
2427
|
+
} catch (e) { /* ignore non-existent providers */ }
|
|
2428
|
+
}
|
|
2429
|
+
}
|
|
2430
|
+
|
|
2431
|
+
// Restore secretary token usage
|
|
2432
|
+
if (data.secretary?.tokenUsage) {
|
|
2433
|
+
Object.assign(company.secretary.tokenUsage, data.secretary.tokenUsage);
|
|
2434
|
+
}
|
|
2435
|
+
if (data.secretary?.hrTokenUsage) {
|
|
2436
|
+
Object.assign(company.secretary.hrAssistant.employee.tokenUsage, data.secretary.hrTokenUsage);
|
|
2437
|
+
}
|
|
2438
|
+
// Restore secretary custom prompt
|
|
2439
|
+
if (data.secretary?.prompt) {
|
|
2440
|
+
company.secretary.prompt = data.secretary.prompt;
|
|
2441
|
+
}
|
|
2442
|
+
// Restore secretary signature
|
|
2443
|
+
if (data.secretary?.signature) {
|
|
2444
|
+
company.secretary.signature = data.secretary.signature;
|
|
2445
|
+
}
|
|
2446
|
+
// Restore secretary CLI backend configuration
|
|
2447
|
+
if (data.secretary?.cliBackend && company.secretary.agentType !== 'cli') {
|
|
2448
|
+
const cliProvider = company.providerRegistry.getById(data.secretary.providerId);
|
|
2449
|
+
if (cliProvider && cliProvider.isCLI && cliProvider.cliBackendId) {
|
|
2450
|
+
const fallback = company.providerRegistry.recommend('general');
|
|
2451
|
+
company.secretary.agent = new CLIAgent({
|
|
2452
|
+
cliBackend: cliProvider.cliBackendId,
|
|
2453
|
+
cliProvider: cliProvider,
|
|
2454
|
+
fallbackProvider: fallback, provider: fallback,
|
|
2455
|
+
});
|
|
2456
|
+
}
|
|
2457
|
+
}
|
|
2458
|
+
// Restore secretary Web agent configuration
|
|
2459
|
+
if (data.secretary?.providerId && company.secretary.agentType !== 'web') {
|
|
2460
|
+
const webProvider = company.providerRegistry.getById(data.secretary.providerId);
|
|
2461
|
+
if (webProvider && webProvider.isWeb) {
|
|
2462
|
+
company.secretary.agent = new WebAgent({ provider: webProvider });
|
|
2463
|
+
// Re-bind employeeId after agent replacement (for per-employee session isolation)
|
|
2464
|
+
company.secretary.agent.setEmployeeId(company.secretary.id);
|
|
2465
|
+
}
|
|
2466
|
+
}
|
|
2467
|
+
|
|
2468
|
+
// Restore secretary memory from separate memory file
|
|
2469
|
+
if (company.secretary?.id) {
|
|
2470
|
+
const secretaryMemory = loadAgentMemory(company.secretary.id);
|
|
2471
|
+
if (secretaryMemory) {
|
|
2472
|
+
company.secretary.memory = Memory.deserialize(secretaryMemory);
|
|
2473
|
+
console.log(` 🧠 Secretary memory restored: ${secretaryMemory.shortTerm?.length || 0} short-term, ${secretaryMemory.longTerm?.length || 0} long-term`);
|
|
2474
|
+
}
|
|
2475
|
+
}
|
|
2476
|
+
|
|
2477
|
+
// Restore departments and Agents
|
|
2478
|
+
company.departments.clear();
|
|
2479
|
+
for (const deptData of (data.departments || [])) {
|
|
2480
|
+
const dept = new Department({
|
|
2481
|
+
name: deptData.name,
|
|
2482
|
+
mission: deptData.mission,
|
|
2483
|
+
company: company.id,
|
|
2484
|
+
});
|
|
2485
|
+
dept.id = deptData.id;
|
|
2486
|
+
dept.status = deptData.status || 'active';
|
|
2487
|
+
dept.leader = deptData.leader;
|
|
2488
|
+
dept.workspacePath = deptData.workspacePath;
|
|
2489
|
+
// Ensure workspace directory exists (may be missing after migration or cleanup)
|
|
2490
|
+
if (dept.workspacePath && !existsSync(dept.workspacePath)) {
|
|
2491
|
+
try { mkdirSync(dept.workspacePath, { recursive: true }); } catch { /* ignore */ }
|
|
2492
|
+
}
|
|
2493
|
+
dept.createdAt = deptData.createdAt ? new Date(deptData.createdAt) : new Date();
|
|
2494
|
+
// Load groupChat from file storage (with legacy inline data migration)
|
|
2495
|
+
dept.loadGroupChatFromStore(deptData.groupChat);
|
|
2496
|
+
|
|
2497
|
+
// Restore Agents
|
|
2498
|
+
for (const agentData of (deptData.members || [])) {
|
|
2499
|
+
// Load memory from separate file (higher priority than serialized data)
|
|
2500
|
+
const externalMemory = loadAgentMemory(agentData.id);
|
|
2501
|
+
if (externalMemory) {
|
|
2502
|
+
agentData.memory = externalMemory;
|
|
2503
|
+
}
|
|
2504
|
+
const agent = deserializeEmployee(agentData, company.providerRegistry);
|
|
2505
|
+
dept.addAgent(agent);
|
|
2506
|
+
// Restore toolkit
|
|
2507
|
+
if (dept.workspacePath) {
|
|
2508
|
+
agent.initToolKit(dept.workspacePath, company.messageBus);
|
|
2509
|
+
}
|
|
2510
|
+
}
|
|
2511
|
+
|
|
2512
|
+
company.departments.set(dept.id, dept);
|
|
2513
|
+
}
|
|
2514
|
+
|
|
2515
|
+
// Restore talent market
|
|
2516
|
+
company.talentMarket.pool.clear();
|
|
2517
|
+
for (const profile of (data.talentPool || [])) {
|
|
2518
|
+
// Restore provider reference
|
|
2519
|
+
if (profile.provider?.id) {
|
|
2520
|
+
profile.provider = company.providerRegistry.getById(profile.provider.id) || profile.provider;
|
|
2521
|
+
}
|
|
2522
|
+
company.talentMarket.pool.set(profile.id, profile);
|
|
2523
|
+
}
|
|
2524
|
+
|
|
2525
|
+
// Restore mailbox, chat history, progress reports, logs
|
|
2526
|
+
company.mailbox = data.mailbox || [];
|
|
2527
|
+
company.chatHistory = data.chatHistory || [];
|
|
2528
|
+
// Restore chat session ID
|
|
2529
|
+
if (data.chatSessionId) {
|
|
2530
|
+
company.chatSessionId = data.chatSessionId;
|
|
2531
|
+
}
|
|
2532
|
+
// If there is old chatHistory data and file storage is empty, migrate it
|
|
2533
|
+
if (company.chatHistory.length > 0 && chatStore.getMessageCount(company.chatSessionId) === 0) {
|
|
2534
|
+
chatStore.migrateFromArray(company.chatSessionId, company.chatHistory);
|
|
2535
|
+
}
|
|
2536
|
+
company.progressReports = data.progressReports || [];
|
|
2537
|
+
company.logs = data.logs || [];
|
|
2538
|
+
|
|
2539
|
+
// Restore message bus
|
|
2540
|
+
if (data.messageBusMessages) {
|
|
2541
|
+
// Only restore history, don't rebuild inbox
|
|
2542
|
+
company.messageBus.messages = data.messageBusMessages.map(m => ({
|
|
2543
|
+
...m,
|
|
2544
|
+
timestamp: new Date(m.timestamp),
|
|
2545
|
+
toJSON() { return m; },
|
|
2546
|
+
}));
|
|
2547
|
+
}
|
|
2548
|
+
|
|
2549
|
+
// Restore requirement manager
|
|
2550
|
+
if (data.requirements) {
|
|
2551
|
+
company.requirementManager = RequirementManager.deserialize(data.requirements);
|
|
2552
|
+
}
|
|
2553
|
+
|
|
2554
|
+
// Restore team manager
|
|
2555
|
+
if (data.teams) {
|
|
2556
|
+
company.teamManager = TeamManager.deserialize(data.teams);
|
|
2557
|
+
}
|
|
2558
|
+
|
|
2559
|
+
// Restore cron jobs
|
|
2560
|
+
if (data.cronJobs) {
|
|
2561
|
+
cronScheduler.restore(data.cronJobs);
|
|
2562
|
+
}
|
|
2563
|
+
|
|
2564
|
+
// Restore CLI backends (custom backends)
|
|
2565
|
+
if (data.cliBackends) {
|
|
2566
|
+
cliBackendRegistry.restore(data.cliBackends);
|
|
2567
|
+
}
|
|
2568
|
+
// Sync CLI backends into provider registry
|
|
2569
|
+
company.providerRegistry.syncCLIBackends(cliBackendRegistry);
|
|
2570
|
+
// Re-apply provider configs for CLI providers (to restore enabled state)
|
|
2571
|
+
const reapplyCLIConfigs = () => {
|
|
2572
|
+
if (data.providerConfigs) {
|
|
2573
|
+
for (const [pid, cfg] of Object.entries(data.providerConfigs)) {
|
|
2574
|
+
if (pid.startsWith('cli-')) {
|
|
2575
|
+
try { company.providerRegistry.configure(pid, cfg.apiKey); } catch {}
|
|
2576
|
+
}
|
|
2577
|
+
}
|
|
2578
|
+
}
|
|
2579
|
+
};
|
|
2580
|
+
reapplyCLIConfigs();
|
|
2581
|
+
// Auto-detect CLI backends in background, then re-sync and re-apply configs
|
|
2582
|
+
cliBackendRegistry.detectAll().then(() => {
|
|
2583
|
+
company.providerRegistry.syncCLIBackends(cliBackendRegistry);
|
|
2584
|
+
reapplyCLIConfigs();
|
|
2585
|
+
}).catch(() => {});
|
|
2586
|
+
|
|
2587
|
+
// Re-start group chat loop for all restored agents
|
|
2588
|
+
// (_initSubsystems ran during constructor when departments were still empty,
|
|
2589
|
+
// so we need to register all agents that were restored afterwards)
|
|
2590
|
+
groupChatLoop.start(company);
|
|
2591
|
+
for (const dept of company.departments.values()) {
|
|
2592
|
+
for (const agent of dept.getMembers()) {
|
|
2593
|
+
groupChatLoop.startAgentLoop(agent);
|
|
2594
|
+
}
|
|
2595
|
+
}
|
|
2596
|
+
|
|
2597
|
+
console.log(`✅ Company "${company.name}" state restored: ${company.departments.size} departments`);
|
|
2598
|
+
return company;
|
|
2599
|
+
}
|
|
2600
|
+
}
|