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,990 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Employee Lifecycle Manager
|
|
3
|
+
*
|
|
4
|
+
* Owns all per-employee behaviour that was previously scattered across
|
|
5
|
+
* GroupChatLoop:
|
|
6
|
+
*
|
|
7
|
+
* - Random-interval poll cycle (read group messages)
|
|
8
|
+
* - Inner monologue / flow-state (InnerMonologue)
|
|
9
|
+
* - "Should I speak?" decision (LLM call + fallback)
|
|
10
|
+
* - Anti-spam / rate-limiting
|
|
11
|
+
* - Self-check for stuck workflow nodes
|
|
12
|
+
* - Idle-chat topic initiation
|
|
13
|
+
* - Prompt building for dept-chat & work-chat
|
|
14
|
+
* - Agent memory (monologue summaries across rounds)
|
|
15
|
+
*
|
|
16
|
+
* The lifecycle is attached to a single Employee and receives a reference
|
|
17
|
+
* to the GroupChatLoop (the thin global coordinator) for event emission,
|
|
18
|
+
* company resolution, and cross-agent nudging.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
22
|
+
import path from 'path';
|
|
23
|
+
import { existsSync } from 'fs';
|
|
24
|
+
import {
|
|
25
|
+
PROMPT,
|
|
26
|
+
getTraitStyle,
|
|
27
|
+
getAgeStyle,
|
|
28
|
+
getFewShotExamples,
|
|
29
|
+
getFallbackReplies,
|
|
30
|
+
} from '../prompts.js';
|
|
31
|
+
import { robustJSONParse } from '../utils/json-parse.js';
|
|
32
|
+
|
|
33
|
+
// ─── File reference expansion ──────────────────────────────────────────
|
|
34
|
+
// Agent writes [[file:path/to/file]], we expand to [[file:deptId:path|displayName]]
|
|
35
|
+
const SIMPLE_FILE_REF = /\[\[file:([^\]|:]+)\]\]/g;
|
|
36
|
+
const INCOMPLETE_FILE_REF = /\[\[file:([^:]+):([^\]|]+)\]\]/g;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Expand short-form file references into full format for the frontend.
|
|
40
|
+
* [[file:src/index.js]] → [[file:dept123:src/index.js|index.js]]
|
|
41
|
+
* [[file:dept123:src/index.js]] → [[file:dept123:src/index.js|index.js]]
|
|
42
|
+
* Only creates clickable references for files that actually exist on disk.
|
|
43
|
+
* Returns { content, invalidRefs } so caller can provide feedback for bad references.
|
|
44
|
+
*/
|
|
45
|
+
function expandFileReferences(content, departmentId, workspacePath) {
|
|
46
|
+
if (!content || !departmentId) return { content, invalidRefs: [] };
|
|
47
|
+
const invalidRefs = [];
|
|
48
|
+
// First: fix incomplete refs [[file:deptId:path]] → [[file:deptId:path|name]]
|
|
49
|
+
let expanded = content.replace(INCOMPLETE_FILE_REF, (_match, deptId, filePath) => {
|
|
50
|
+
const trimmed = filePath.trim();
|
|
51
|
+
if (workspacePath) {
|
|
52
|
+
const fullPath = path.join(workspacePath, trimmed);
|
|
53
|
+
if (!existsSync(fullPath)) {
|
|
54
|
+
invalidRefs.push(trimmed);
|
|
55
|
+
return trimmed;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
const displayName = path.basename(trimmed);
|
|
59
|
+
return `[[file:${deptId}:${trimmed}|${displayName}]]`;
|
|
60
|
+
});
|
|
61
|
+
// Then: expand simple refs [[file:path]] → [[file:deptId:path|name]]
|
|
62
|
+
expanded = expanded.replace(SIMPLE_FILE_REF, (_match, filePath) => {
|
|
63
|
+
const trimmed = filePath.trim();
|
|
64
|
+
if (workspacePath) {
|
|
65
|
+
const fullPath = path.join(workspacePath, trimmed);
|
|
66
|
+
if (!existsSync(fullPath)) {
|
|
67
|
+
invalidRefs.push(trimmed);
|
|
68
|
+
return trimmed;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
const displayName = path.basename(trimmed);
|
|
72
|
+
return `[[file:${departmentId}:${trimmed}|${displayName}]]`;
|
|
73
|
+
});
|
|
74
|
+
return { content: expanded, invalidRefs };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// ─── Default Config ────────────────────────────────────────────────────
|
|
78
|
+
const DEFAULT_CONFIG = {
|
|
79
|
+
pollIntervalMinMs: 10000, // 10 s
|
|
80
|
+
pollIntervalMaxMs: 300000, // 5 min
|
|
81
|
+
idleChatThresholdMs: 3600000, // 1 h
|
|
82
|
+
maxInnerMonologueLen: 5,
|
|
83
|
+
maxGroupMessagesPerTurn: 1,
|
|
84
|
+
debounceMs: 3000,
|
|
85
|
+
antiSpamWindowMs: 300000, // 5 min
|
|
86
|
+
antiSpamMaxMessages: 2,
|
|
87
|
+
cooldownAfterSpeakMs: 60000, // 60 s
|
|
88
|
+
selfCheckIntervalMs: 120000, // 120 s
|
|
89
|
+
stuckThresholdMs: 180000, // 3 min
|
|
90
|
+
// Department casual chat — relaxed
|
|
91
|
+
deptAntiSpamWindowMs: 600000,
|
|
92
|
+
deptAntiSpamMaxMessages: 4,
|
|
93
|
+
deptCooldownAfterSpeakMs: 30000,
|
|
94
|
+
deptIdleChatThresholdMs: 600000,
|
|
95
|
+
topicSaturationThreshold: 7,
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
// ─── InnerMonologue (flow state) ───────────────────────────────────────
|
|
99
|
+
export class InnerMonologue {
|
|
100
|
+
constructor(agentId, agentName, groupId) {
|
|
101
|
+
this.id = uuidv4();
|
|
102
|
+
this.agentId = agentId;
|
|
103
|
+
this.agentName = agentName;
|
|
104
|
+
this.groupId = groupId;
|
|
105
|
+
this.thoughts = [];
|
|
106
|
+
this.startedAt = new Date();
|
|
107
|
+
this.status = 'thinking'; // thinking | decided | done
|
|
108
|
+
this.decision = null;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
addThought(content) {
|
|
112
|
+
this.thoughts.push({ id: uuidv4(), content, timestamp: new Date() });
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
toJSON() {
|
|
116
|
+
return {
|
|
117
|
+
id: this.id,
|
|
118
|
+
agentId: this.agentId,
|
|
119
|
+
agentName: this.agentName,
|
|
120
|
+
groupId: this.groupId,
|
|
121
|
+
thoughts: this.thoughts,
|
|
122
|
+
startedAt: this.startedAt,
|
|
123
|
+
status: this.status,
|
|
124
|
+
decision: this.decision,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// ─── EmployeeLifecycle ─────────────────────────────────────────────────
|
|
130
|
+
export class EmployeeLifecycle {
|
|
131
|
+
/**
|
|
132
|
+
* @param {import('./base-employee.js').Employee} employee
|
|
133
|
+
* @param {object} [config] Override individual config keys
|
|
134
|
+
*/
|
|
135
|
+
constructor(employee, config = {}) {
|
|
136
|
+
this.employee = employee;
|
|
137
|
+
this.config = { ...DEFAULT_CONFIG, ...config };
|
|
138
|
+
|
|
139
|
+
// Per-group state key = groupId
|
|
140
|
+
this._lastReadIndex = new Map(); // groupId → number
|
|
141
|
+
this._lastProcessedVisible = new Map(); // groupId → number
|
|
142
|
+
this._recentSpeaks = new Map(); // groupId → Date[]
|
|
143
|
+
this._lastSelfCheck = new Map(); // groupId → Date
|
|
144
|
+
this._agentMemory = new Map(); // groupId → string[]
|
|
145
|
+
this._activeMonologues = new Map(); // groupId → InnerMonologue
|
|
146
|
+
this._monologueHistory = new Map(); // groupId → InnerMonologue[]
|
|
147
|
+
this._lastGroupActivity = new Map(); // groupId → Date
|
|
148
|
+
|
|
149
|
+
// Processing guards
|
|
150
|
+
this._processing = new Set(); // groupId
|
|
151
|
+
|
|
152
|
+
// Poll timer (single timer for the employee)
|
|
153
|
+
this._pollTimer = null;
|
|
154
|
+
|
|
155
|
+
// Back-reference to global coordinator (set externally)
|
|
156
|
+
this._coordinator = null;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
160
|
+
// Public API
|
|
161
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
162
|
+
|
|
163
|
+
/** Attach the global coordinator (GroupChatLoop) so we can emit events & nudge peers. */
|
|
164
|
+
setCoordinator(coordinator) {
|
|
165
|
+
this._coordinator = coordinator;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** Start the random-interval poll loop for this employee. */
|
|
169
|
+
start() {
|
|
170
|
+
if (this._pollTimer) return; // already running
|
|
171
|
+
this._scheduleNext();
|
|
172
|
+
console.log(` 🔄 [Lifecycle] ${this.employee.name} started (${this.config.pollIntervalMinMs}-${this.config.pollIntervalMaxMs}ms)`);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** Stop polling. */
|
|
176
|
+
stop() {
|
|
177
|
+
if (this._pollTimer) {
|
|
178
|
+
clearTimeout(this._pollTimer);
|
|
179
|
+
this._pollTimer = null;
|
|
180
|
+
}
|
|
181
|
+
this._processing.clear();
|
|
182
|
+
this._recentSpeaks.clear();
|
|
183
|
+
this._lastSelfCheck.clear();
|
|
184
|
+
this._lastProcessedVisible.clear();
|
|
185
|
+
// NOTE: _agentMemory is NOT cleared — memories persist across stop/start
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** Trigger a delayed check for a specific group (e.g. when someone @mentions this employee). */
|
|
189
|
+
async triggerCheck(groupId) {
|
|
190
|
+
const delay = this._randomDelay();
|
|
191
|
+
console.log(` 📨 [Lifecycle] ${this.employee.name} will check ${groupId} in ${Math.round(delay / 1000)}s`);
|
|
192
|
+
setTimeout(async () => {
|
|
193
|
+
try {
|
|
194
|
+
if (!this._isRunning()) return;
|
|
195
|
+
await this._processGroupMessages(groupId, false);
|
|
196
|
+
} catch (err) {
|
|
197
|
+
console.error(` ❌ [Lifecycle] ${this.employee.name} delayed trigger error:`, err.message);
|
|
198
|
+
}
|
|
199
|
+
}, delay);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// ─── Query helpers (used by GroupChatLoop / API) ────────────────────
|
|
203
|
+
|
|
204
|
+
getActiveMonologue(groupId) {
|
|
205
|
+
return this._activeMonologues.get(groupId) || null;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
getMonologueHistory(groupId, limit = 10) {
|
|
209
|
+
return (this._monologueHistory.get(groupId) || []).slice(-limit);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
getActiveThinking() {
|
|
213
|
+
const result = [];
|
|
214
|
+
for (const [, monologue] of this._activeMonologues) {
|
|
215
|
+
if (monologue.status === 'thinking') {
|
|
216
|
+
result.push({
|
|
217
|
+
agentId: monologue.agentId,
|
|
218
|
+
agentName: monologue.agentName,
|
|
219
|
+
groupId: monologue.groupId,
|
|
220
|
+
startedAt: monologue.startedAt,
|
|
221
|
+
thoughtCount: monologue.thoughts.length,
|
|
222
|
+
status: 'thinking',
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
return result;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// ─── Serialization ──────────────────────────────────────────────────
|
|
230
|
+
|
|
231
|
+
serialize() {
|
|
232
|
+
const obj = (map) => {
|
|
233
|
+
const o = {};
|
|
234
|
+
for (const [k, v] of map) o[k] = v;
|
|
235
|
+
return o;
|
|
236
|
+
};
|
|
237
|
+
return {
|
|
238
|
+
lastReadIndex: obj(this._lastReadIndex),
|
|
239
|
+
lastProcessedVisible: obj(this._lastProcessedVisible),
|
|
240
|
+
agentMemory: obj(this._agentMemory),
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
restore(data) {
|
|
245
|
+
if (!data) return;
|
|
246
|
+
const load = (map, raw) => { if (raw) for (const [k, v] of Object.entries(raw)) map.set(k, v); };
|
|
247
|
+
load(this._lastReadIndex, data.lastReadIndex);
|
|
248
|
+
load(this._lastProcessedVisible, data.lastProcessedVisible);
|
|
249
|
+
load(this._agentMemory, data.agentMemory);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
253
|
+
// Internal — Poll Cycle
|
|
254
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
255
|
+
|
|
256
|
+
_scheduleNext() {
|
|
257
|
+
if (!this._isRunning()) return;
|
|
258
|
+
const delay = this._randomDelay();
|
|
259
|
+
this._pollTimer = setTimeout(async () => {
|
|
260
|
+
try {
|
|
261
|
+
await this._pollCycle();
|
|
262
|
+
} catch (err) {
|
|
263
|
+
console.error(` ❌ [Lifecycle] ${this.employee.name} poll error:`, err.message);
|
|
264
|
+
}
|
|
265
|
+
this._scheduleNext();
|
|
266
|
+
}, delay);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
async _pollCycle() {
|
|
270
|
+
if (!this._isRunning()) return;
|
|
271
|
+
const agent = this.employee;
|
|
272
|
+
if (agent.status === 'dismissed') return;
|
|
273
|
+
|
|
274
|
+
// Periodically clean expired short-term memories
|
|
275
|
+
agent.memory.cleanExpiredShortTerm();
|
|
276
|
+
|
|
277
|
+
const groups = this._getAgentGroups();
|
|
278
|
+
for (const group of groups) {
|
|
279
|
+
await this._checkGroupForAgent(group);
|
|
280
|
+
await this._selfCheckWorkflow(group);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
285
|
+
// Internal — Group discovery
|
|
286
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
287
|
+
|
|
288
|
+
_getAgentGroups() {
|
|
289
|
+
const company = this._getCompany();
|
|
290
|
+
if (!company) return [];
|
|
291
|
+
|
|
292
|
+
const agent = this.employee;
|
|
293
|
+
const groups = [];
|
|
294
|
+
|
|
295
|
+
// 1. Requirement work groups
|
|
296
|
+
const requirements = company.requirementManager.listAll();
|
|
297
|
+
for (const req of requirements) {
|
|
298
|
+
if (req.status !== 'in_progress' && req.status !== 'planning') continue;
|
|
299
|
+
const dept = company.findDepartment(req.departmentId);
|
|
300
|
+
if (!dept || !dept.agents.has(agent.id)) continue;
|
|
301
|
+
groups.push({
|
|
302
|
+
id: req.id,
|
|
303
|
+
title: req.title,
|
|
304
|
+
departmentId: req.departmentId,
|
|
305
|
+
type: 'work',
|
|
306
|
+
messages: req.groupChat || [],
|
|
307
|
+
requirement: req,
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// 2. Department general chat groups
|
|
312
|
+
for (const dept of company.departments.values()) {
|
|
313
|
+
if (!dept.agents.has(agent.id)) continue;
|
|
314
|
+
if (dept.status === 'disbanded') continue;
|
|
315
|
+
groups.push({
|
|
316
|
+
id: `dept-${dept.id}`,
|
|
317
|
+
title: `${dept.name} Department Chat`,
|
|
318
|
+
departmentId: dept.id,
|
|
319
|
+
type: 'chat',
|
|
320
|
+
messages: dept.groupChat || [],
|
|
321
|
+
department: dept,
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
return groups;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
329
|
+
// Internal — Check & process messages
|
|
330
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
331
|
+
|
|
332
|
+
async _checkGroupForAgent(group) {
|
|
333
|
+
const groupId = group.id;
|
|
334
|
+
if (this._processing.has(groupId)) return;
|
|
335
|
+
|
|
336
|
+
const lastRead = this._lastReadIndex.get(groupId) || 0;
|
|
337
|
+
const messages = group.messages;
|
|
338
|
+
|
|
339
|
+
if (messages.length <= lastRead) {
|
|
340
|
+
if (group.type === 'chat') await this._maybeInitiateChat(group);
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
const unreadMessages = messages.slice(lastRead);
|
|
345
|
+
this._lastReadIndex.set(groupId, messages.length);
|
|
346
|
+
|
|
347
|
+
const agent = this.employee;
|
|
348
|
+
const isMentioned = unreadMessages
|
|
349
|
+
.filter(msg => msg.visibility !== 'flow')
|
|
350
|
+
.some(msg => this._isMentionedInMessage(msg));
|
|
351
|
+
|
|
352
|
+
// Any group-visible message from others (including system summaries/reports)
|
|
353
|
+
// should trigger the monologue flow — the agent reads them, thinks, and
|
|
354
|
+
// may choose to stay silent. The key is to *enter* the thinking process.
|
|
355
|
+
const othersMessages = unreadMessages.filter(msg =>
|
|
356
|
+
msg.from?.id !== agent.id && msg.visibility !== 'flow'
|
|
357
|
+
);
|
|
358
|
+
|
|
359
|
+
if (othersMessages.length === 0 && !isMentioned) return;
|
|
360
|
+
|
|
361
|
+
await this._processGroupMessages(groupId, isMentioned);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Core logic — process unread messages for a group.
|
|
366
|
+
*/
|
|
367
|
+
async _processGroupMessages(groupId, isMentioned = false) {
|
|
368
|
+
if (this._processing.has(groupId)) return;
|
|
369
|
+
this._processing.add(groupId);
|
|
370
|
+
|
|
371
|
+
const agent = this.employee;
|
|
372
|
+
const company = this._getCompany();
|
|
373
|
+
|
|
374
|
+
// NOTE: wakeUp follows lazy-loading principle.
|
|
375
|
+
// The employee will be automatically woken up on their first chat()
|
|
376
|
+
// call via _ensureSession(). No need to pre-initialize here.
|
|
377
|
+
|
|
378
|
+
try {
|
|
379
|
+
const isDeptChat = groupId.startsWith('dept-');
|
|
380
|
+
let dept, chatTarget, contextTitle;
|
|
381
|
+
|
|
382
|
+
let allVisibleMessages;
|
|
383
|
+
if (isDeptChat) {
|
|
384
|
+
const deptId = groupId.replace('dept-', '');
|
|
385
|
+
dept = company.findDepartment(deptId);
|
|
386
|
+
if (!dept) return;
|
|
387
|
+
chatTarget = dept;
|
|
388
|
+
contextTitle = `${dept.name} Department Chat`;
|
|
389
|
+
allVisibleMessages = (dept.groupChat || []).filter(m => m.visibility !== 'flow');
|
|
390
|
+
} else {
|
|
391
|
+
const requirement = company.requirementManager.get(groupId);
|
|
392
|
+
if (!requirement) return;
|
|
393
|
+
dept = company.findDepartment(requirement.departmentId);
|
|
394
|
+
if (!dept) return;
|
|
395
|
+
chatTarget = requirement;
|
|
396
|
+
contextTitle = requirement.title;
|
|
397
|
+
allVisibleMessages = (requirement.groupChat || []).filter(m => m.visibility !== 'flow');
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// ── Per-employee context scene management ──
|
|
401
|
+
// Switch the employee's active context if the group has changed.
|
|
402
|
+
// For web agents this injects the scene prompt into the existing ChatGPT conversation
|
|
403
|
+
// without re-sending memory+prompt (which was done once at wake-up).
|
|
404
|
+
if (agent.switchContext) {
|
|
405
|
+
const contextType = isDeptChat ? 'dept-chat' : 'work-chat';
|
|
406
|
+
const members = dept.getMembers().map(a => `${a.name}(${a.role})`).join(', ');
|
|
407
|
+
const scenePrompt = isDeptChat
|
|
408
|
+
? `You are now in the "${contextTitle}" group.\nMembers: ${members}\nThis is a casual department chat. Respond naturally in your personality.`
|
|
409
|
+
: `You are now in the "${contextTitle}" work group.\nMembers: ${members}\nThis is a task-focused discussion. Stay on topic and contribute professionally.`;
|
|
410
|
+
await agent.switchContext({
|
|
411
|
+
contextId: groupId,
|
|
412
|
+
contextType,
|
|
413
|
+
contextTitle,
|
|
414
|
+
scenePrompt,
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// Distinguish read vs unread
|
|
419
|
+
const contextKey = groupId;
|
|
420
|
+
const lastProcessedCount = this._lastProcessedVisible.get(contextKey) || 0;
|
|
421
|
+
const readContext = allVisibleMessages.slice(0, lastProcessedCount).slice(-10);
|
|
422
|
+
const unreadNew = allVisibleMessages.slice(lastProcessedCount);
|
|
423
|
+
this._lastProcessedVisible.set(contextKey, allVisibleMessages.length);
|
|
424
|
+
|
|
425
|
+
let recentMessages = [
|
|
426
|
+
...readContext.map(m => ({ ...m, _isRead: true })),
|
|
427
|
+
...unreadNew.map(m => ({ ...m, _isRead: false })),
|
|
428
|
+
];
|
|
429
|
+
|
|
430
|
+
if (unreadNew.length === 0 && !isMentioned) return;
|
|
431
|
+
|
|
432
|
+
// Jitter wait (5-20s)
|
|
433
|
+
const jitter = 5000 + Math.random() * 15000;
|
|
434
|
+
await this._delay(jitter);
|
|
435
|
+
|
|
436
|
+
// Re-fetch fresh messages after waiting
|
|
437
|
+
let freshVisibleMessages;
|
|
438
|
+
if (isDeptChat) {
|
|
439
|
+
freshVisibleMessages = (chatTarget.groupChat || []).filter(m => m.visibility !== 'flow');
|
|
440
|
+
} else {
|
|
441
|
+
freshVisibleMessages = (chatTarget.groupChat || []).filter(m => m.visibility !== 'flow');
|
|
442
|
+
}
|
|
443
|
+
if (freshVisibleMessages.length > allVisibleMessages.length) {
|
|
444
|
+
const extraMessages = freshVisibleMessages.slice(allVisibleMessages.length);
|
|
445
|
+
recentMessages = [
|
|
446
|
+
...recentMessages,
|
|
447
|
+
...extraMessages.map(m => ({ ...m, _isRead: false })),
|
|
448
|
+
];
|
|
449
|
+
this._lastProcessedVisible.set(contextKey, freshVisibleMessages.length);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// Create flow state
|
|
453
|
+
const monologue = new InnerMonologue(agent.id, agent.name, groupId);
|
|
454
|
+
this._activeMonologues.set(groupId, monologue);
|
|
455
|
+
|
|
456
|
+
this._emit('monologue:start', {
|
|
457
|
+
agentId: agent.id, agentName: agent.name, groupId,
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
// LLM flow thinking
|
|
461
|
+
const thinkingResult = await this._agentThink(
|
|
462
|
+
{ id: groupId, title: contextTitle }, dept, recentMessages, isMentioned, monologue
|
|
463
|
+
);
|
|
464
|
+
|
|
465
|
+
if (monologue.thoughts.length === 0 && thinkingResult.reason) {
|
|
466
|
+
monologue.addThought(thinkingResult.reason);
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// Write inner monologue to group stream (boss peek)
|
|
470
|
+
const innerThoughtsContent = thinkingResult.innerThoughts || thinkingResult.reason || null;
|
|
471
|
+
if (innerThoughtsContent) {
|
|
472
|
+
chatTarget.addGroupMessage(
|
|
473
|
+
{ id: agent.id, name: agent.name, avatar: agent.avatar, role: agent.role },
|
|
474
|
+
innerThoughtsContent,
|
|
475
|
+
'monologue',
|
|
476
|
+
'flow'
|
|
477
|
+
);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// Topic saturation gate
|
|
481
|
+
const topicSaturation = thinkingResult.topicSaturation || 0;
|
|
482
|
+
if (thinkingResult.shouldSpeak && topicSaturation >= this.config.topicSaturationThreshold && !isMentioned) {
|
|
483
|
+
console.log(` 🎯 [Lifecycle] ${agent.name} silenced by topic saturation (score=${topicSaturation})`);
|
|
484
|
+
thinkingResult.shouldSpeak = false;
|
|
485
|
+
thinkingResult.reason = `Topic saturation: ${topicSaturation}/10`;
|
|
486
|
+
monologue.addThought(PROMPT.monologue.topicSaturated(topicSaturation));
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// Cooldown check
|
|
490
|
+
if (thinkingResult.shouldSpeak) {
|
|
491
|
+
const spamRecheck = this._getSpamInfo(groupId, isDeptChat);
|
|
492
|
+
if (spamRecheck.isOnCooldown) {
|
|
493
|
+
console.log(` 🕐 [Lifecycle] ${agent.name} on cooldown, suppressing`);
|
|
494
|
+
thinkingResult.shouldSpeak = false;
|
|
495
|
+
thinkingResult.reason = 'On cooldown after recent speak';
|
|
496
|
+
monologue.addThought(PROMPT.monologue.cooldownSilence);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
// Send messages
|
|
501
|
+
if (thinkingResult.shouldSpeak) {
|
|
502
|
+
const messagesToSend = thinkingResult.messages || [];
|
|
503
|
+
for (const msg of messagesToSend.slice(0, this.config.maxGroupMessagesPerTurn)) {
|
|
504
|
+
// Expand [[file:path]] → [[file:deptId:path|name]] for frontend rendering
|
|
505
|
+
const { content: expandedContent, invalidRefs } = expandFileReferences(msg.content, dept.id, dept.workspacePath);
|
|
506
|
+
chatTarget.addGroupMessage(
|
|
507
|
+
{ id: agent.id, name: agent.name, avatar: agent.avatar, role: agent.role },
|
|
508
|
+
expandedContent,
|
|
509
|
+
'message'
|
|
510
|
+
);
|
|
511
|
+
monologue.addThought(`[Sent to group] ${expandedContent}`);
|
|
512
|
+
|
|
513
|
+
// Auto-feedback: notify agent about invalid file references
|
|
514
|
+
if (invalidRefs.length > 0) {
|
|
515
|
+
const invalidList = invalidRefs.map(f => ` - ${f}`).join('\n');
|
|
516
|
+
chatTarget.addGroupMessage(
|
|
517
|
+
{ id: 'system', name: 'System', role: 'system' },
|
|
518
|
+
`⚠️ @[${agent.id}] File reference error: the following files do not exist in workspace:\n${invalidList}\nPlease use workspace_files or file_search tool to check available files before referencing them.`,
|
|
519
|
+
'message', null, { auto: true }
|
|
520
|
+
);
|
|
521
|
+
}
|
|
522
|
+
this._lastGroupActivity.set(groupId, new Date());
|
|
523
|
+
this._recordSpeak(groupId);
|
|
524
|
+
|
|
525
|
+
// Nudge other group members
|
|
526
|
+
const allGroupMembers = dept.getMembers();
|
|
527
|
+
for (const member of allGroupMembers) {
|
|
528
|
+
if (member.id === agent.id) continue;
|
|
529
|
+
const nudgeDelay = this._randomDelay();
|
|
530
|
+
setTimeout(() => {
|
|
531
|
+
this._nudgePeer(member.id, groupId);
|
|
532
|
+
}, nudgeDelay);
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
// Save company state
|
|
536
|
+
company.save();
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
// Finalise monologue
|
|
540
|
+
monologue.status = 'done';
|
|
541
|
+
monologue.finishedAt = Date.now();
|
|
542
|
+
monologue.decision = thinkingResult.shouldSpeak ? 'spoke' : 'silent';
|
|
543
|
+
|
|
544
|
+
if (!this._monologueHistory.has(groupId)) this._monologueHistory.set(groupId, []);
|
|
545
|
+
const history = this._monologueHistory.get(groupId);
|
|
546
|
+
history.push(monologue);
|
|
547
|
+
if (history.length > 20) this._monologueHistory.set(groupId, history.slice(-20));
|
|
548
|
+
|
|
549
|
+
this._activeMonologues.delete(groupId);
|
|
550
|
+
|
|
551
|
+
// Save agent memory — now uses the structured Memory system.
|
|
552
|
+
// The AI-driven memoryOps (processed in _agentThink) handle long-term/short-term memory.
|
|
553
|
+
// Here we still keep a lightweight _agentMemory for backward compat (e.g. monologue peek).
|
|
554
|
+
if (!this._agentMemory.has(groupId)) this._agentMemory.set(groupId, []);
|
|
555
|
+
const legacyMemory = this._agentMemory.get(groupId);
|
|
556
|
+
const memoryThoughts = monologue.thoughts
|
|
557
|
+
.filter(t => !t.content.startsWith('[Sent to group]') && !t.content.startsWith('[Self-regulation]'))
|
|
558
|
+
.map(t => t.content);
|
|
559
|
+
if (memoryThoughts.length > 0) {
|
|
560
|
+
const summary = memoryThoughts[memoryThoughts.length - 1];
|
|
561
|
+
const action = monologue.decision === 'spoke' ? '[spoke]' : '[silent]';
|
|
562
|
+
legacyMemory.push(`${action} ${summary.slice(0, 200)}`);
|
|
563
|
+
if (legacyMemory.length > 10) this._agentMemory.set(groupId, legacyMemory.slice(-10));
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
this._emit('monologue:end', {
|
|
567
|
+
agentId: agent.id, agentName: agent.name, groupId,
|
|
568
|
+
decision: monologue.decision,
|
|
569
|
+
thoughtCount: monologue.thoughts.length,
|
|
570
|
+
thoughts: monologue.thoughts,
|
|
571
|
+
reason: thinkingResult.reason || '',
|
|
572
|
+
});
|
|
573
|
+
|
|
574
|
+
} catch (error) {
|
|
575
|
+
console.error(` ❌ [Lifecycle] ${agent.name} process error in ${groupId}:`, error.message);
|
|
576
|
+
this._activeMonologues.delete(groupId);
|
|
577
|
+
} finally {
|
|
578
|
+
this._processing.delete(groupId);
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
583
|
+
// Internal — LLM thinking
|
|
584
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
585
|
+
|
|
586
|
+
async _agentThink(requirement, dept, recentMessages, isMentioned, monologue) {
|
|
587
|
+
const agent = this.employee;
|
|
588
|
+
const members = dept.getMembers().map(a => ({ id: a.id, name: a.name, role: a.role }));
|
|
589
|
+
|
|
590
|
+
// Format messages — includes sender id, name, time
|
|
591
|
+
const readMessages = recentMessages.filter(m => m._isRead);
|
|
592
|
+
const unreadMessages = recentMessages.filter(m => !m._isRead);
|
|
593
|
+
|
|
594
|
+
const formatMsg = (msg) => {
|
|
595
|
+
const senderName = msg.from?.name || 'Unknown';
|
|
596
|
+
const senderId = msg.from?.id || 'unknown';
|
|
597
|
+
const time = new Date(msg.time).toLocaleTimeString('zh', { hour: '2-digit', minute: '2-digit' });
|
|
598
|
+
if (msg.type === 'system') return `[System] ${msg.content}`;
|
|
599
|
+
const selfMark = msg.from?.id === agent.id ? ' (you)' : '';
|
|
600
|
+
return `[${time}] ${senderName}(${senderId})${selfMark}: ${msg.content}`;
|
|
601
|
+
};
|
|
602
|
+
|
|
603
|
+
// Dedupe warning
|
|
604
|
+
const colleagueReplies = unreadMessages.filter(m =>
|
|
605
|
+
m.from?.id !== agent.id && m.from?.id !== 'boss' && members.some(mem => mem.id === m.from?.id)
|
|
606
|
+
);
|
|
607
|
+
const dedupeWarning = colleagueReplies.length > 0
|
|
608
|
+
? PROMPT.context.dedupeWarning(
|
|
609
|
+
colleagueReplies.length,
|
|
610
|
+
colleagueReplies.map(m => `- ${m.from?.name}: "${(m.content || '').slice(0, 80)}"`).join('\n')
|
|
611
|
+
)
|
|
612
|
+
: '';
|
|
613
|
+
|
|
614
|
+
// Random angle seed
|
|
615
|
+
const angles = PROMPT.angles;
|
|
616
|
+
const myAngle = angles[Math.floor(Math.random() * angles.length)];
|
|
617
|
+
const angleHint = PROMPT.context.angleHint(myAngle);
|
|
618
|
+
|
|
619
|
+
let chatContext = '';
|
|
620
|
+
if (readMessages.length > 0) {
|
|
621
|
+
chatContext += PROMPT.context.readHeader + '\n';
|
|
622
|
+
chatContext += readMessages.map(formatMsg).join('\n');
|
|
623
|
+
chatContext += '\n\n';
|
|
624
|
+
}
|
|
625
|
+
if (unreadMessages.length > 0) {
|
|
626
|
+
chatContext += PROMPT.context.unreadHeader + '\n';
|
|
627
|
+
chatContext += unreadMessages.map(formatMsg).join('\n');
|
|
628
|
+
} else {
|
|
629
|
+
chatContext += PROMPT.context.noNewMessages;
|
|
630
|
+
}
|
|
631
|
+
chatContext += dedupeWarning;
|
|
632
|
+
chatContext += angleHint;
|
|
633
|
+
|
|
634
|
+
// Build structured memory context from the employee's Memory system
|
|
635
|
+
// This includes: rolling history summary + long-term memories + short-term memories + relationship impressions
|
|
636
|
+
const participantIds = members.filter(m => m.id !== agent.id).map(m => m.id);
|
|
637
|
+
const memoryContext = agent.memory.buildFullContext(requirement.id, participantIds);
|
|
638
|
+
|
|
639
|
+
const p = agent.personality;
|
|
640
|
+
|
|
641
|
+
// Anti-spam context
|
|
642
|
+
const groupId = requirement.id;
|
|
643
|
+
const isDeptChat = groupId.startsWith('dept-');
|
|
644
|
+
const spamInfo = this._getSpamInfo(groupId, isDeptChat);
|
|
645
|
+
|
|
646
|
+
const systemPrompt = isDeptChat
|
|
647
|
+
? this._buildDeptChatPrompt(p, requirement, members, memoryContext, spamInfo, isMentioned)
|
|
648
|
+
: this._buildWorkChatPrompt(p, requirement, members, memoryContext, spamInfo, isMentioned);
|
|
649
|
+
|
|
650
|
+
// Thinking peers
|
|
651
|
+
const thinkingPeers = this._getThinkingPeers(groupId);
|
|
652
|
+
const thinkingInfo = thinkingPeers.length > 0
|
|
653
|
+
? PROMPT.context.thinkingPeers(thinkingPeers.join(', '))
|
|
654
|
+
: '';
|
|
655
|
+
|
|
656
|
+
const userPrompt = isDeptChat
|
|
657
|
+
? PROMPT.userPrompt.deptChat(chatContext, thinkingInfo, agent.name, agent.age, agent.personality.trait)
|
|
658
|
+
: PROMPT.userPrompt.workChat(chatContext, thinkingInfo, agent.name, agent.age, agent.personality.trait);
|
|
659
|
+
|
|
660
|
+
try {
|
|
661
|
+
if (!agent.canChat()) {
|
|
662
|
+
return this._fallbackThink(groupId, isMentioned, recentMessages);
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
const response = await agent.chat([
|
|
666
|
+
{ role: 'system', content: systemPrompt },
|
|
667
|
+
{ role: 'user', content: userPrompt },
|
|
668
|
+
], { temperature: 0.95, maxTokens: 1024 });
|
|
669
|
+
|
|
670
|
+
const rawContent = response.content || '';
|
|
671
|
+
const result = robustJSONParse(rawContent);
|
|
672
|
+
|
|
673
|
+
const thoughtContent = (result.innerThoughts && result.innerThoughts.trim())
|
|
674
|
+
? result.innerThoughts.trim()
|
|
675
|
+
: result.reason
|
|
676
|
+
? `[Inner thought] ${result.reason}`
|
|
677
|
+
: `[Read group messages, processing...]`;
|
|
678
|
+
monologue.addThought(thoughtContent);
|
|
679
|
+
|
|
680
|
+
// ── Process AI-driven memory management ──
|
|
681
|
+
// 1. Rolling history summary: AI compresses old messages into a summary
|
|
682
|
+
if (result.memorySummary && typeof result.memorySummary === 'string' && result.memorySummary.trim()) {
|
|
683
|
+
agent.memory.updateHistorySummary(groupId, result.memorySummary.trim());
|
|
684
|
+
console.log(` 📜 [Lifecycle] ${agent.name} updated history summary for ${groupId} (${result.memorySummary.trim().length} chars)`);
|
|
685
|
+
}
|
|
686
|
+
// 2. Memory operations: AI adds/updates/deletes its own memories
|
|
687
|
+
if (result.memoryOps && Array.isArray(result.memoryOps) && result.memoryOps.length > 0) {
|
|
688
|
+
const memResult = agent.memory.processMemoryOps(result.memoryOps);
|
|
689
|
+
console.log(` 🧠 [Lifecycle] ${agent.name} memory ops: +${memResult.added} ~${memResult.updated} -${memResult.deleted}`);
|
|
690
|
+
}
|
|
691
|
+
// 3. Relationship impressions: AI updates its personal views of colleagues
|
|
692
|
+
if (result.relationshipOps && Array.isArray(result.relationshipOps) && result.relationshipOps.length > 0) {
|
|
693
|
+
const relResult = agent.memory.processRelationshipOps(result.relationshipOps);
|
|
694
|
+
console.log(` 👥 [Lifecycle] ${agent.name} relationship updates: ${relResult.updated}`);
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
// Anti-spam gate
|
|
698
|
+
const spamCheck = this._getSpamInfo(requirement.id, isDeptChat);
|
|
699
|
+
const maxMessages = isDeptChat ? this.config.deptAntiSpamMaxMessages : this.config.antiSpamMaxMessages;
|
|
700
|
+
if (result.shouldSpeak && spamCheck.recentCount >= maxMessages) {
|
|
701
|
+
console.log(` 🔇 [Lifecycle] ${agent.name} suppressed (anti-spam: ${spamCheck.recentCount} msgs)`);
|
|
702
|
+
result.shouldSpeak = false;
|
|
703
|
+
result.reason = `Anti-spam: already sent ${spamCheck.recentCount} messages recently`;
|
|
704
|
+
result.messages = [];
|
|
705
|
+
monologue.addThought(`[Self-regulation] I wanted to speak but I've been too active recently. Better stay quiet.`);
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
if (result.messages && result.messages.length > 1) {
|
|
709
|
+
result.messages = [result.messages[0]];
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
return {
|
|
713
|
+
shouldSpeak: !!result.shouldSpeak,
|
|
714
|
+
messages: (result.messages || []).filter(m => m.content && m.content.trim()),
|
|
715
|
+
reason: result.reason || '',
|
|
716
|
+
innerThoughts: thoughtContent,
|
|
717
|
+
topicSaturation: typeof result.topicSaturation === 'number' ? result.topicSaturation : 0,
|
|
718
|
+
};
|
|
719
|
+
|
|
720
|
+
} catch (error) {
|
|
721
|
+
console.warn(` ⚠️ [Lifecycle] ${agent.name} LLM think error:`, error.message);
|
|
722
|
+
return this._fallbackThink(groupId, isMentioned, recentMessages);
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
727
|
+
// Internal — Prompt building
|
|
728
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
729
|
+
|
|
730
|
+
_buildDeptChatPrompt(p, requirement, members, memoryContext, spamInfo, isMentioned) {
|
|
731
|
+
const agent = this.employee;
|
|
732
|
+
const genderLabel = PROMPT.genderLabel[agent.gender] || PROMPT.genderLabel.male;
|
|
733
|
+
const ageStyle = getAgeStyle(agent.age);
|
|
734
|
+
const traitStyle = getTraitStyle(p.trait);
|
|
735
|
+
const fewShot = getFewShotExamples(p.trait);
|
|
736
|
+
const memberList = members.map(m => `${m.name}(${m.role})`).join(', ');
|
|
737
|
+
const pt = PROMPT.deptChat;
|
|
738
|
+
return `${traitStyle}
|
|
739
|
+
|
|
740
|
+
${pt.intro(agent.name, genderLabel, agent.age, agent.role, p.tone, p.quirk, agent.signature)}
|
|
741
|
+
|
|
742
|
+
${pt.ageIntro}
|
|
743
|
+
${ageStyle}
|
|
744
|
+
|
|
745
|
+
---
|
|
746
|
+
|
|
747
|
+
${pt.groupContext(requirement.title, memberList)}
|
|
748
|
+
${memoryContext}
|
|
749
|
+
|
|
750
|
+
${pt.examplesHeader}
|
|
751
|
+
|
|
752
|
+
${fewShot}
|
|
753
|
+
|
|
754
|
+
${pt.rules(spamInfo.recentCount, isMentioned)}
|
|
755
|
+
|
|
756
|
+
${pt.topicSaturation}
|
|
757
|
+
|
|
758
|
+
${pt.outputFormat}
|
|
759
|
+
|
|
760
|
+
${pt.antiAIWarning(agent.age)}`;
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
_buildWorkChatPrompt(p, requirement, members, memoryContext, spamInfo, isMentioned) {
|
|
764
|
+
const agent = this.employee;
|
|
765
|
+
const genderLabel = PROMPT.genderLabel[agent.gender] || PROMPT.genderLabel.male;
|
|
766
|
+
const ageStyle = getAgeStyle(agent.age);
|
|
767
|
+
const traitStyle = getTraitStyle(p.trait);
|
|
768
|
+
const fewShot = getFewShotExamples(p.trait);
|
|
769
|
+
const memberList = members.map(m => `${m.name}(${m.role})`).join(', ');
|
|
770
|
+
const pt = PROMPT.workChat;
|
|
771
|
+
return `${traitStyle}
|
|
772
|
+
|
|
773
|
+
${pt.intro(agent.name, genderLabel, agent.age, agent.role, p.tone, p.quirk, agent.signature)}
|
|
774
|
+
|
|
775
|
+
${pt.ageIntro}
|
|
776
|
+
${ageStyle}
|
|
777
|
+
|
|
778
|
+
---
|
|
779
|
+
|
|
780
|
+
${pt.groupContext(requirement.title, memberList)}
|
|
781
|
+
${memoryContext}
|
|
782
|
+
|
|
783
|
+
${pt.examplesHeader}
|
|
784
|
+
|
|
785
|
+
${fewShot}
|
|
786
|
+
|
|
787
|
+
${pt.shouldSpeak}
|
|
788
|
+
|
|
789
|
+
${pt.shouldNotSpeak(spamInfo.recentCount, spamInfo.isOnCooldown, isMentioned)}
|
|
790
|
+
|
|
791
|
+
${pt.topicSaturation}
|
|
792
|
+
|
|
793
|
+
${pt.outputFormat}
|
|
794
|
+
|
|
795
|
+
${pt.antiAIWarning(agent.age)}`;
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
799
|
+
// Internal — Fallback thinking
|
|
800
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
801
|
+
|
|
802
|
+
_fallbackThink(groupId, isMentioned, recentMessages) {
|
|
803
|
+
const agent = this.employee;
|
|
804
|
+
const isDeptChat = groupId.startsWith('dept-');
|
|
805
|
+
const spamCheck = this._getSpamInfo(groupId, isDeptChat);
|
|
806
|
+
const maxMessages = isDeptChat ? this.config.deptAntiSpamMaxMessages : this.config.antiSpamMaxMessages;
|
|
807
|
+
if (spamCheck.recentCount >= maxMessages) {
|
|
808
|
+
return { shouldSpeak: false, messages: [], reason: 'Anti-spam: too many recent messages' };
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
const trait = agent.personality?.trait || '';
|
|
812
|
+
const replies = getFallbackReplies(trait);
|
|
813
|
+
|
|
814
|
+
if (isDeptChat) {
|
|
815
|
+
const lastMsg = recentMessages[recentMessages.length - 1];
|
|
816
|
+
if (lastMsg && lastMsg.from?.id !== agent.id) {
|
|
817
|
+
return { shouldSpeak: true, messages: [{ content: replies.dept }], reason: 'Department chat fallback' };
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
const lastMsg = recentMessages[recentMessages.length - 1];
|
|
822
|
+
if (lastMsg && lastMsg.from?.id === 'boss') {
|
|
823
|
+
return { shouldSpeak: true, messages: [{ content: replies.boss }], reason: 'Boss message fallback' };
|
|
824
|
+
}
|
|
825
|
+
if (isMentioned) {
|
|
826
|
+
return { shouldSpeak: true, messages: [{ content: replies.mention }], reason: 'Mentioned fallback' };
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
return { shouldSpeak: false, messages: [], reason: 'LLM unavailable, staying silent' };
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
833
|
+
// Internal — Anti-spam
|
|
834
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
835
|
+
|
|
836
|
+
_recordSpeak(groupId) {
|
|
837
|
+
if (!this._recentSpeaks.has(groupId)) this._recentSpeaks.set(groupId, []);
|
|
838
|
+
this._recentSpeaks.get(groupId).push(Date.now());
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
_getSpamInfo(groupId, isDeptChat = false) {
|
|
842
|
+
const timestamps = this._recentSpeaks.get(groupId) || [];
|
|
843
|
+
const now = Date.now();
|
|
844
|
+
const windowMs = isDeptChat ? this.config.deptAntiSpamWindowMs : this.config.antiSpamWindowMs;
|
|
845
|
+
const cooldownMs = isDeptChat ? this.config.deptCooldownAfterSpeakMs : this.config.cooldownAfterSpeakMs;
|
|
846
|
+
const recentTimestamps = timestamps.filter(t => t > now - windowMs);
|
|
847
|
+
this._recentSpeaks.set(groupId, recentTimestamps);
|
|
848
|
+
|
|
849
|
+
return {
|
|
850
|
+
recentCount: recentTimestamps.length,
|
|
851
|
+
isOnCooldown: recentTimestamps.some(t => t > now - cooldownMs),
|
|
852
|
+
};
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
856
|
+
// Internal — Workflow self-check
|
|
857
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
858
|
+
|
|
859
|
+
async _selfCheckWorkflow(group) {
|
|
860
|
+
if (!group.requirement?.workflow?.nodes) return;
|
|
861
|
+
|
|
862
|
+
const agent = this.employee;
|
|
863
|
+
const groupId = group.id;
|
|
864
|
+
const now = Date.now();
|
|
865
|
+
const lastCheck = this._lastSelfCheck.get(groupId) || 0;
|
|
866
|
+
if (now - lastCheck < this.config.selfCheckIntervalMs) return;
|
|
867
|
+
|
|
868
|
+
const nodes = group.requirement.workflow.nodes;
|
|
869
|
+
const myStuckNodes = nodes.filter(n => {
|
|
870
|
+
if (n.assigneeId !== agent.id && n.reviewerId !== agent.id) return false;
|
|
871
|
+
if (!['running', 'reviewing', 'revision'].includes(n.status)) return false;
|
|
872
|
+
if (!n.startedAt) return false;
|
|
873
|
+
return now - new Date(n.startedAt).getTime() > this.config.stuckThresholdMs;
|
|
874
|
+
});
|
|
875
|
+
if (myStuckNodes.length === 0) return;
|
|
876
|
+
|
|
877
|
+
this._lastSelfCheck.set(groupId, now);
|
|
878
|
+
|
|
879
|
+
const recentGroupMessages = (group.requirement.groupChat || [])
|
|
880
|
+
.filter(m => m.visibility !== 'flow').slice(-10);
|
|
881
|
+
const myRecentMessages = recentGroupMessages.filter(m => m.from?.id === agent.id && m.type === 'message');
|
|
882
|
+
if (myRecentMessages.length > 0) {
|
|
883
|
+
const lastMsgTime = new Date(myRecentMessages[myRecentMessages.length - 1].time).getTime();
|
|
884
|
+
if (now - lastMsgTime < this.config.selfCheckIntervalMs * 2) return;
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
const spamCheck = this._getSpamInfo(groupId);
|
|
888
|
+
if (spamCheck.recentCount >= this.config.antiSpamMaxMessages) return;
|
|
889
|
+
|
|
890
|
+
for (const node of myStuckNodes.slice(0, 1)) {
|
|
891
|
+
const elapsed = Math.round((now - new Date(node.startedAt).getTime()) / 1000);
|
|
892
|
+
const isReviewer = node.reviewerId === agent.id;
|
|
893
|
+
|
|
894
|
+
let checkInContent;
|
|
895
|
+
if (isReviewer && node.status === 'reviewing') {
|
|
896
|
+
checkInContent = `🔍 Still reviewing "${node.title}" (${elapsed}s elapsed). Working on it...`;
|
|
897
|
+
} else if (node.status === 'revision') {
|
|
898
|
+
checkInContent = `✏️ Working on revisions for "${node.title}" (${elapsed}s elapsed). Making progress...`;
|
|
899
|
+
} else {
|
|
900
|
+
checkInContent = `⚙️ Still working on "${node.title}" (${elapsed}s elapsed). Will update when I have results.`;
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
group.requirement.addGroupMessage(
|
|
904
|
+
{ id: agent.id, name: agent.name, avatar: agent.avatar, role: agent.role },
|
|
905
|
+
checkInContent,
|
|
906
|
+
'message', null, { auto: true }
|
|
907
|
+
);
|
|
908
|
+
|
|
909
|
+
this._recordSpeak(groupId);
|
|
910
|
+
this._lastGroupActivity.set(groupId, new Date());
|
|
911
|
+
console.log(` 🔔 [Lifecycle] ${agent.name} self-check: ${checkInContent}`);
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
916
|
+
// Internal — Idle chat initiation
|
|
917
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
918
|
+
|
|
919
|
+
async _maybeInitiateChat(group) {
|
|
920
|
+
const lastActivity = this._lastGroupActivity.get(group.id);
|
|
921
|
+
if (!lastActivity) return;
|
|
922
|
+
|
|
923
|
+
const idleMs = Date.now() - lastActivity.getTime();
|
|
924
|
+
const threshold = group.type === 'chat' ? this.config.deptIdleChatThresholdMs : this.config.idleChatThresholdMs;
|
|
925
|
+
if (idleMs < threshold) return;
|
|
926
|
+
if (group.type !== 'chat') return;
|
|
927
|
+
if (Math.random() > 0.15) return;
|
|
928
|
+
|
|
929
|
+
console.log(` 💬 [Lifecycle] ${this.employee.name} considering topic in idle group ${group.title}`);
|
|
930
|
+
await this._processGroupMessages(group.id, false);
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
934
|
+
// Internal — Mention detection
|
|
935
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
936
|
+
|
|
937
|
+
_isMentionedInMessage(message) {
|
|
938
|
+
const agent = this.employee;
|
|
939
|
+
if (!message.content || typeof message.content !== 'string') return false;
|
|
940
|
+
const content = message.content;
|
|
941
|
+
if (content.includes(`@[${agent.id}]`)) return true;
|
|
942
|
+
if (content.includes(`@${agent.name}`)) return true;
|
|
943
|
+
if (content.includes('@all') || content.includes('@everyone')) return true;
|
|
944
|
+
return false;
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
948
|
+
// Internal — Helpers
|
|
949
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
950
|
+
|
|
951
|
+
_isRunning() {
|
|
952
|
+
return this._coordinator?.running ?? false;
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
_getCompany() {
|
|
956
|
+
return this._coordinator?.company ?? null;
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
_randomDelay() {
|
|
960
|
+
return this.config.pollIntervalMinMs +
|
|
961
|
+
Math.random() * (this.config.pollIntervalMaxMs - this.config.pollIntervalMinMs);
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
_delay(ms) {
|
|
965
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
_emit(event, data) {
|
|
969
|
+
this._coordinator?.emit(event, data);
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
/** Nudge another employee to check a group via the coordinator. */
|
|
973
|
+
_nudgePeer(peerId, groupId) {
|
|
974
|
+
this._coordinator?.nudgeAgent(peerId, groupId);
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
/** Get names of peers currently thinking in the same group (via coordinator). */
|
|
978
|
+
_getThinkingPeers(groupId) {
|
|
979
|
+
if (!this._coordinator) return [];
|
|
980
|
+
const peers = [];
|
|
981
|
+
// Ask coordinator for all active monologues in this group (from all employees)
|
|
982
|
+
const allThinking = this._coordinator.getActiveThinkingAgents();
|
|
983
|
+
for (const t of allThinking) {
|
|
984
|
+
if (t.groupId === groupId && t.agentId !== this.employee.id) {
|
|
985
|
+
peers.push(t.agentName);
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
return peers;
|
|
989
|
+
}
|
|
990
|
+
}
|