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,1789 @@
|
|
|
1
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { existsSync, readFileSync } from 'fs';
|
|
4
|
+
import { chatStore } from './agent/chat-store.js';
|
|
5
|
+
import { WorkspaceManager } from './workspace.js';
|
|
6
|
+
import { robustJSONParse } from './utils/json-parse.js';
|
|
7
|
+
|
|
8
|
+
/** Group chat prefix for requirement group chats in chatStore */
|
|
9
|
+
const REQ_GROUP_PREFIX = 'req-';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Requirement status enum
|
|
13
|
+
*/
|
|
14
|
+
export const RequirementStatus = {
|
|
15
|
+
PENDING: 'pending', // Just created, awaiting assignment
|
|
16
|
+
PLANNING: 'planning', // Leader is decomposing workflow
|
|
17
|
+
IN_PROGRESS: 'in_progress', // In progress
|
|
18
|
+
PENDING_APPROVAL: 'pending_approval', // All tasks done, awaiting Boss approval
|
|
19
|
+
COMPLETED: 'completed', // Completed (Boss approved)
|
|
20
|
+
FAILED: 'failed', // Failed
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Workflow node status
|
|
25
|
+
*/
|
|
26
|
+
export const TaskNodeStatus = {
|
|
27
|
+
WAITING: 'waiting', // Waiting for dependencies
|
|
28
|
+
READY: 'ready', // Ready to start
|
|
29
|
+
RUNNING: 'running', // Running
|
|
30
|
+
REVIEWING: 'reviewing', // Completed execution, under review
|
|
31
|
+
REVISION: 'revision', // Review rejected, needs revision
|
|
32
|
+
COMPLETED: 'completed', // Completed (and passed review if reviewer assigned)
|
|
33
|
+
FAILED: 'failed', // Failed
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Requirement data model
|
|
38
|
+
*/
|
|
39
|
+
export class Requirement {
|
|
40
|
+
constructor({ title, description, departmentId, departmentName, bossMessage }) {
|
|
41
|
+
this.id = uuidv4();
|
|
42
|
+
this.title = title;
|
|
43
|
+
this.description = description;
|
|
44
|
+
this.departmentId = departmentId;
|
|
45
|
+
this.departmentName = departmentName;
|
|
46
|
+
this.bossMessage = bossMessage; // Boss's original message
|
|
47
|
+
this.status = RequirementStatus.PENDING;
|
|
48
|
+
this.workflow = null; // Workflow (decomposed by leader)
|
|
49
|
+
this.groupChat = []; // Group chat messages
|
|
50
|
+
this.outputs = []; // Output results
|
|
51
|
+
this.createdAt = new Date();
|
|
52
|
+
this.startedAt = null;
|
|
53
|
+
this.completedAt = null;
|
|
54
|
+
this.summary = null; // Post-completion execution summary
|
|
55
|
+
|
|
56
|
+
// Live progress status (dynamically updated during execution)
|
|
57
|
+
this.liveStatus = {
|
|
58
|
+
currentNodeId: null, // Currently executing node ID
|
|
59
|
+
currentNodeTitle: null, // Currently executing node title
|
|
60
|
+
currentAgent: null, // Currently executing Agent name
|
|
61
|
+
currentAction: null, // Current action description (e.g. "calling LLM", "executing tool file_write")
|
|
62
|
+
lastActiveAt: null, // Last active time
|
|
63
|
+
heartbeat: null, // Heartbeat time (updated on each LLM/tool call)
|
|
64
|
+
toolCallsInProgress: [], // Tool calls in progress
|
|
65
|
+
recentFileChanges: [], // Recent file change records
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Update live status */
|
|
70
|
+
updateLiveStatus(updates) {
|
|
71
|
+
Object.assign(this.liveStatus, updates, { lastActiveAt: new Date(), heartbeat: new Date() });
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Record file change */
|
|
75
|
+
addFileChange(agentName, filePath, action = 'write') {
|
|
76
|
+
this.liveStatus.recentFileChanges.push({
|
|
77
|
+
agentName,
|
|
78
|
+
filePath,
|
|
79
|
+
action,
|
|
80
|
+
time: new Date(),
|
|
81
|
+
});
|
|
82
|
+
// Keep only last 100 entries (more generous to ensure files tab shows all files)
|
|
83
|
+
if (this.liveStatus.recentFileChanges.length > 100) {
|
|
84
|
+
this.liveStatus.recentFileChanges = this.liveStatus.recentFileChanges.slice(-100);
|
|
85
|
+
}
|
|
86
|
+
this.liveStatus.lastActiveAt = new Date();
|
|
87
|
+
this.liveStatus.heartbeat = new Date();
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Add group chat message
|
|
92
|
+
* @param {object} from - Sender info
|
|
93
|
+
* @param {string} content - Message content
|
|
94
|
+
* @param {string} type - Message type: message | system | tool_call | output
|
|
95
|
+
* @param {string|null} visibility - Visibility: 'group' (broadcast to group chat) | 'flow' (flow log, only visible to self and boss)
|
|
96
|
+
* - tool_call type defaults to 'flow' (flow log, work process doesn't flood the group chat)
|
|
97
|
+
* - Other types default to 'group' (broadcast to group chat)
|
|
98
|
+
* @param {object} options - Extra options { auto: boolean } - mark as auto-generated message
|
|
99
|
+
*/
|
|
100
|
+
addGroupMessage(from, content, type = 'message', visibility = null, options = {}) {
|
|
101
|
+
// Auto-infer visibility: tool_call and output types default to 'flow', others default to 'group'
|
|
102
|
+
const resolvedVisibility = visibility || (type === 'tool_call' || type === 'output' ? 'flow' : 'group');
|
|
103
|
+
const msg = {
|
|
104
|
+
id: uuidv4(),
|
|
105
|
+
from: {
|
|
106
|
+
id: from.id || 'system',
|
|
107
|
+
name: from.name || 'System',
|
|
108
|
+
avatar: from.avatar || null,
|
|
109
|
+
role: from.role || null,
|
|
110
|
+
},
|
|
111
|
+
content,
|
|
112
|
+
type, // message | system | tool_call | output
|
|
113
|
+
visibility: resolvedVisibility, // group | flow
|
|
114
|
+
time: new Date(),
|
|
115
|
+
...(options.auto ? { auto: true } : {}),
|
|
116
|
+
};
|
|
117
|
+
this.groupChat.push(msg);
|
|
118
|
+
// Persist to file storage (non-blocking, fire-and-forget)
|
|
119
|
+
try { chatStore.appendGroupMessage(`${REQ_GROUP_PREFIX}${this.id}`, msg); } catch {}
|
|
120
|
+
// Sync update heartbeat
|
|
121
|
+
this.liveStatus.heartbeat = new Date();
|
|
122
|
+
this.liveStatus.lastActiveAt = new Date();
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** Add output */
|
|
126
|
+
addOutput(agentId, agentName, role, outputType, content, metadata = {}) {
|
|
127
|
+
this.outputs.push({
|
|
128
|
+
id: uuidv4(),
|
|
129
|
+
agentId,
|
|
130
|
+
agentName,
|
|
131
|
+
role,
|
|
132
|
+
outputType, // text | code | image | file
|
|
133
|
+
content,
|
|
134
|
+
metadata,
|
|
135
|
+
createdAt: new Date(),
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** Serialize (groupChat is stored in separate files, not included here) */
|
|
140
|
+
serialize() {
|
|
141
|
+
return {
|
|
142
|
+
id: this.id,
|
|
143
|
+
title: this.title,
|
|
144
|
+
description: this.description,
|
|
145
|
+
departmentId: this.departmentId,
|
|
146
|
+
departmentName: this.departmentName,
|
|
147
|
+
bossMessage: this.bossMessage,
|
|
148
|
+
status: this.status,
|
|
149
|
+
workflow: this.workflow,
|
|
150
|
+
// groupChat is persisted in chatStore files (data/chats/group-req-{id}/)
|
|
151
|
+
outputs: this.outputs,
|
|
152
|
+
createdAt: this.createdAt,
|
|
153
|
+
startedAt: this.startedAt,
|
|
154
|
+
completedAt: this.completedAt,
|
|
155
|
+
summary: this.summary,
|
|
156
|
+
liveStatus: this.liveStatus,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** Deserialize */
|
|
161
|
+
static deserialize(data) {
|
|
162
|
+
const req = new Requirement({
|
|
163
|
+
title: data.title,
|
|
164
|
+
description: data.description,
|
|
165
|
+
departmentId: data.departmentId,
|
|
166
|
+
departmentName: data.departmentName,
|
|
167
|
+
bossMessage: data.bossMessage,
|
|
168
|
+
});
|
|
169
|
+
req.id = data.id;
|
|
170
|
+
req.status = data.status;
|
|
171
|
+
req.workflow = data.workflow;
|
|
172
|
+
|
|
173
|
+
// Load groupChat from file storage; migrate legacy inline data if present
|
|
174
|
+
const groupId = `${REQ_GROUP_PREFIX}${req.id}`;
|
|
175
|
+
if (data.groupChat && data.groupChat.length > 0) {
|
|
176
|
+
// Legacy data found inline — migrate to file storage, then discard from state
|
|
177
|
+
chatStore.migrateGroupChat(groupId, data.groupChat);
|
|
178
|
+
}
|
|
179
|
+
req.groupChat = chatStore.getGroupMessages(groupId, 500);
|
|
180
|
+
|
|
181
|
+
req.outputs = data.outputs || [];
|
|
182
|
+
req.createdAt = data.createdAt ? new Date(data.createdAt) : new Date();
|
|
183
|
+
req.startedAt = data.startedAt ? new Date(data.startedAt) : null;
|
|
184
|
+
req.completedAt = data.completedAt ? new Date(data.completedAt) : null;
|
|
185
|
+
req.summary = data.summary;
|
|
186
|
+
req.liveStatus = data.liveStatus || {
|
|
187
|
+
currentNodeId: null, currentNodeTitle: null, currentAgent: null,
|
|
188
|
+
currentAction: null, lastActiveAt: null, heartbeat: null,
|
|
189
|
+
toolCallsInProgress: [], recentFileChanges: [],
|
|
190
|
+
};
|
|
191
|
+
return req;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Requirement Manager
|
|
197
|
+
* Manages the lifecycle of all company requirements
|
|
198
|
+
*/
|
|
199
|
+
export class RequirementManager {
|
|
200
|
+
constructor() {
|
|
201
|
+
this.requirements = new Map();
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/** Create requirement */
|
|
205
|
+
create(data) {
|
|
206
|
+
const req = new Requirement(data);
|
|
207
|
+
this.requirements.set(req.id, req);
|
|
208
|
+
return req;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/** Get requirement */
|
|
212
|
+
get(id) {
|
|
213
|
+
return this.requirements.get(id);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/** List all requirements */
|
|
217
|
+
listAll() {
|
|
218
|
+
return [...this.requirements.values()].sort(
|
|
219
|
+
(a, b) => new Date(b.createdAt) - new Date(a.createdAt)
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/** List requirements by department */
|
|
224
|
+
listByDepartment(departmentId) {
|
|
225
|
+
return this.listAll().filter(r => r.departmentId === departmentId);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/** List requirements by status */
|
|
229
|
+
listByStatus(status) {
|
|
230
|
+
return this.listAll().filter(r => r.status === status);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Leader decomposes requirement into workflow.
|
|
235
|
+
* Automatically finds a capable agent from members to perform the decomposition.
|
|
236
|
+
* @param {Requirement} requirement - Requirement
|
|
237
|
+
* @param {Array} members - Department members list (Employee instances)
|
|
238
|
+
* @param {object} [adjustmentContext] - If present, this is a workflow adjustment
|
|
239
|
+
* @returns {object} Workflow
|
|
240
|
+
*/
|
|
241
|
+
async planWorkflow(requirement, members, adjustmentContext = null) {
|
|
242
|
+
requirement.status = RequirementStatus.PLANNING;
|
|
243
|
+
if (adjustmentContext) {
|
|
244
|
+
requirement.addGroupMessage(
|
|
245
|
+
{ name: 'System', role: 'system' },
|
|
246
|
+
`🔄 Adjusting workflow based on Boss's instructions...`,
|
|
247
|
+
'system', null, { auto: true }
|
|
248
|
+
);
|
|
249
|
+
} else {
|
|
250
|
+
requirement.addGroupMessage(
|
|
251
|
+
{ name: 'System', role: 'system' },
|
|
252
|
+
`📋 Requirement "${requirement.title}" created, leader is decomposing the workflow...`,
|
|
253
|
+
'system', null, { auto: true }
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Find a member who can chat (LLM or CLI with chat ability) to do the decomposition.
|
|
258
|
+
// Prefer leader, then fall back to any member who canChat().
|
|
259
|
+
const leader = members.find(m => m.role === 'Project Leader') || members[0];
|
|
260
|
+
let planner = leader?.canChat() ? leader : null;
|
|
261
|
+
if (!planner) {
|
|
262
|
+
planner = members.find(m => m.canChat()) || null;
|
|
263
|
+
}
|
|
264
|
+
if (planner && planner !== leader) {
|
|
265
|
+
console.log(` 🔄 Leader cannot chat, borrowing [${planner.name}] for workflow decomposition`);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Build member info
|
|
269
|
+
const memberInfo = members.map(m => ({
|
|
270
|
+
id: m.id,
|
|
271
|
+
name: m.name,
|
|
272
|
+
role: m.role,
|
|
273
|
+
skills: m.skills,
|
|
274
|
+
}));
|
|
275
|
+
|
|
276
|
+
const systemPrompt = `You are a project leader who needs to decompose a requirement into an executable workflow.
|
|
277
|
+
|
|
278
|
+
Team members:
|
|
279
|
+
${JSON.stringify(memberInfo, null, 2)}
|
|
280
|
+
|
|
281
|
+
Please decompose the requirement into a workflow (DAG - Directed Acyclic Graph) with multiple task nodes. Tasks can have dependency relationships.
|
|
282
|
+
Output in JSON format:
|
|
283
|
+
{
|
|
284
|
+
"nodes": [
|
|
285
|
+
{
|
|
286
|
+
"id": "node_1",
|
|
287
|
+
"title": "Task title",
|
|
288
|
+
"description": "Detailed description",
|
|
289
|
+
"assigneeId": "Assignee ID",
|
|
290
|
+
"assigneeName": "Assignee name",
|
|
291
|
+
"dependencies": [],
|
|
292
|
+
"estimatedMinutes": 5,
|
|
293
|
+
"outputType": "text|code|file",
|
|
294
|
+
"reviewerId": "Reviewer agent ID (optional, null if no review needed)",
|
|
295
|
+
"reviewerName": "Reviewer name",
|
|
296
|
+
"reviewCriteria": "Specific review criteria the reviewer should check (optional)"
|
|
297
|
+
}
|
|
298
|
+
],
|
|
299
|
+
"summary": "Workflow overview"
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
Requirements:
|
|
303
|
+
1. Task granularity should be moderate, each task assigned to one person
|
|
304
|
+
2. **MAXIMIZE PARALLELISM**: Tasks that can run in parallel MUST not be serialized. Prefer wide parallel DAGs over deep serial chains
|
|
305
|
+
3. dependencies should contain the dependent node id array, empty array if no dependencies
|
|
306
|
+
4. The leader can handle "integration and review" type tasks
|
|
307
|
+
5. assigneeId must be selected from team members
|
|
308
|
+
6. Return JSON only, no other content
|
|
309
|
+
7. **Extremely important: Not every member needs to participate!** Only assign people who are truly needed. Members unrelated to the requirement should not be given tasks. Better to leave people idle than to create busywork
|
|
310
|
+
8. Task nodes should be lean and efficient. Simple requirements only need 1-3 nodes, avoid over-decomposition
|
|
311
|
+
9. Each task description should be clear and specific, allowing the assignee to complete it in one go, avoiding multiple iterations
|
|
312
|
+
10. **Encourage collaboration**: When multiple agents work in parallel, their tasks should be designed to have natural interaction points. Include in descriptions that they should coordinate with parallel teammates via send_message
|
|
313
|
+
11. **REVIEW MECHANISM**: For complex or important tasks, you MAY assign a reviewer (reviewerId). The reviewer will audit the work for significant issues. Review rules:
|
|
314
|
+
- The reviewer MUST be a different person from the assignee (never review your own work)
|
|
315
|
+
- For tasks where two agents work in parallel, they can review EACH OTHER's work (Agent A reviews Agent B, Agent B reviews Agent A)
|
|
316
|
+
- The leader can serve as reviewer for critical integration tasks
|
|
317
|
+
- reviewCriteria should be specific and measurable (e.g. "Check that all API endpoints have error handling and input validation" rather than vague "review the code")
|
|
318
|
+
- MOST tasks do NOT need a reviewer. Only assign reviewers for genuinely complex, high-risk tasks. Simple or medium tasks should skip review (set reviewerId to null)
|
|
319
|
+
- When in doubt, do NOT assign a reviewer — over-reviewing slows down the workflow significantly`;
|
|
320
|
+
|
|
321
|
+
const userPrompt = adjustmentContext
|
|
322
|
+
? `Requirement title: ${requirement.title}\nRequirement description: ${requirement.description}\n\n**ADJUSTMENT REQUEST FROM BOSS:**\n${adjustmentContext.bossMessage}\n\n**YOUR PLANNED ADJUSTMENTS:**\n${adjustmentContext.adjustments}\n\n**PREVIOUS WORKFLOW (for reference):**\n${adjustmentContext.previousWorkflow}\n\n**EXISTING OUTPUT FILES (must be preserved and built upon):**\n${adjustmentContext.existingOutputs || 'None'}\n\n**IMPORTANT:** This is an ADJUSTMENT, NOT a restart. You must:\n1. PRESERVE all existing output files - do NOT recreate them from scratch\n2. Only create tasks that MODIFY existing files or ADD new content\n3. When a task needs to change an existing file, the agent should READ the current file first, then modify it\n4. Only add NEW tasks for genuinely new work that wasn't done before\n5. Reuse the previous workflow structure where possible, adjusting only what the Boss requested\n\nPlease create an ADJUSTED workflow based on the Boss's instructions.`
|
|
323
|
+
: `Requirement title: ${requirement.title}\nRequirement description: ${requirement.description}\n\nPlease decompose the workflow.`;
|
|
324
|
+
|
|
325
|
+
if (!planner) {
|
|
326
|
+
console.error('No member can chat, falling back to rule-based workflow');
|
|
327
|
+
const fallbackWorkflow = this._fallbackWorkflow(requirement, members);
|
|
328
|
+
requirement.addGroupMessage(
|
|
329
|
+
{ name: 'System', role: 'system' },
|
|
330
|
+
`⚠️ No available agent for decomposition, generated a simple workflow using rules (${fallbackWorkflow.nodes.length} tasks)`,
|
|
331
|
+
'system', null, { auto: true }
|
|
332
|
+
);
|
|
333
|
+
return fallbackWorkflow;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
try {
|
|
337
|
+
const response = await planner.chat([
|
|
338
|
+
{ role: 'system', content: systemPrompt },
|
|
339
|
+
{ role: 'user', content: userPrompt },
|
|
340
|
+
], { temperature: 0.7, maxTokens: 2048 });
|
|
341
|
+
|
|
342
|
+
// Parse JSON (robust extraction for both LLM and CLI output)
|
|
343
|
+
const workflow = robustJSONParse(response.content);
|
|
344
|
+
|
|
345
|
+
// Validate and complete
|
|
346
|
+
const memberIds = new Set(members.map(m => m.id));
|
|
347
|
+
workflow.nodes = (workflow.nodes || []).map(node => ({
|
|
348
|
+
...node,
|
|
349
|
+
id: node.id || `node_${uuidv4().slice(0, 8)}`,
|
|
350
|
+
status: TaskNodeStatus.WAITING,
|
|
351
|
+
dependencies: (node.dependencies || []).filter(d =>
|
|
352
|
+
workflow.nodes.some(n => n.id === d)
|
|
353
|
+
),
|
|
354
|
+
assigneeId: memberIds.has(node.assigneeId) ? node.assigneeId : members[0]?.id,
|
|
355
|
+
reviewerId: node.reviewerId && memberIds.has(node.reviewerId) ? node.reviewerId : null,
|
|
356
|
+
reviewerName: node.reviewerName || null,
|
|
357
|
+
reviewCriteria: node.reviewCriteria || null,
|
|
358
|
+
reviewRounds: 0, // Review iteration round counter
|
|
359
|
+
maxReviewRounds: 10, // Max review iterations (prevents infinite loops)
|
|
360
|
+
result: null,
|
|
361
|
+
startedAt: null,
|
|
362
|
+
completedAt: null,
|
|
363
|
+
}));
|
|
364
|
+
|
|
365
|
+
// Nodes with no dependencies are marked as ready
|
|
366
|
+
workflow.nodes.forEach(node => {
|
|
367
|
+
if (node.dependencies.length === 0) {
|
|
368
|
+
node.status = TaskNodeStatus.READY;
|
|
369
|
+
}
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
requirement.workflow = workflow;
|
|
373
|
+
|
|
374
|
+
// Group chat notification
|
|
375
|
+
const leader = members.find(m => m.role === 'Project Leader') || members[0];
|
|
376
|
+
requirement.addGroupMessage(
|
|
377
|
+
leader,
|
|
378
|
+
`📊 Workflow decomposition complete! ${workflow.nodes.length} task nodes in total.\n\n${workflow.summary || ''}\n\n${workflow.nodes.map((n, i) =>
|
|
379
|
+
`${i + 1}. [${n.assigneeName || 'TBD'}] ${n.title}${n.dependencies.length > 0 ? ` (depends on: ${n.dependencies.join(', ')})` : ' (can start immediately)'}${n.reviewerId ? ` 🔍 Reviewer: ${n.reviewerName || n.reviewerId}` : ''}`
|
|
380
|
+
).join('\n')}`,
|
|
381
|
+
'message', null, { auto: true }
|
|
382
|
+
);
|
|
383
|
+
|
|
384
|
+
return workflow;
|
|
385
|
+
} catch (e) {
|
|
386
|
+
// LLM failed, generate simple workflow using rules
|
|
387
|
+
console.error('Workflow decomposition failed:', e.message);
|
|
388
|
+
const fallbackWorkflow = this._fallbackWorkflow(requirement, members);
|
|
389
|
+
requirement.addGroupMessage(
|
|
390
|
+
{ name: 'System', role: 'system' },
|
|
391
|
+
`⚠️ AI decomposition failed, generated a simple workflow using rules (${fallbackWorkflow.nodes.length} tasks)`,
|
|
392
|
+
'system', null, { auto: true }
|
|
393
|
+
);
|
|
394
|
+
return fallbackWorkflow;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* Fallback: generate simple serial workflow
|
|
400
|
+
*/
|
|
401
|
+
_fallbackWorkflow(requirement, members) {
|
|
402
|
+
const leader = members.find(m => m.role === 'Project Leader') || members[0];
|
|
403
|
+
const workers = members.filter(m => m.id !== leader?.id);
|
|
404
|
+
|
|
405
|
+
const nodes = [];
|
|
406
|
+
|
|
407
|
+
// Simple requirements only need one core worker, not everyone
|
|
408
|
+
// Pick the most suitable worker (first non-leader member)
|
|
409
|
+
const primaryWorker = workers[0];
|
|
410
|
+
|
|
411
|
+
if (primaryWorker) {
|
|
412
|
+
// Simple mode: one person directly executes the core task
|
|
413
|
+
nodes.push({
|
|
414
|
+
id: 'node_work_0',
|
|
415
|
+
title: `${primaryWorker.role}: Execute requirement`,
|
|
416
|
+
description: `Directly execute requirement "${requirement.title}": ${requirement.description}. Please complete all work in one go and output the final result.`,
|
|
417
|
+
assigneeId: primaryWorker.id,
|
|
418
|
+
assigneeName: primaryWorker.name,
|
|
419
|
+
dependencies: [],
|
|
420
|
+
status: TaskNodeStatus.READY,
|
|
421
|
+
outputType: 'text',
|
|
422
|
+
result: null,
|
|
423
|
+
startedAt: null,
|
|
424
|
+
completedAt: null,
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
// Only add a leader integration step when there are many members (>2)
|
|
428
|
+
if (leader && workers.length > 2) {
|
|
429
|
+
// Assign a second worker to assist
|
|
430
|
+
const secondWorker = workers[1];
|
|
431
|
+
if (secondWorker) {
|
|
432
|
+
nodes.push({
|
|
433
|
+
id: 'node_work_1',
|
|
434
|
+
title: `${secondWorker.role}: Assist with work`,
|
|
435
|
+
description: `Assist in completing the parts of requirement "${requirement.title}" related to your expertise`,
|
|
436
|
+
assigneeId: secondWorker.id,
|
|
437
|
+
assigneeName: secondWorker.name,
|
|
438
|
+
dependencies: [],
|
|
439
|
+
status: TaskNodeStatus.READY,
|
|
440
|
+
outputType: 'text',
|
|
441
|
+
result: null,
|
|
442
|
+
startedAt: null,
|
|
443
|
+
completedAt: null,
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
nodes.push({
|
|
448
|
+
id: 'node_integrate',
|
|
449
|
+
title: 'Results integration and delivery',
|
|
450
|
+
description: 'Consolidate all member outputs and produce the final deliverable',
|
|
451
|
+
assigneeId: leader.id,
|
|
452
|
+
assigneeName: leader.name,
|
|
453
|
+
dependencies: nodes.map(n => n.id),
|
|
454
|
+
status: TaskNodeStatus.WAITING,
|
|
455
|
+
outputType: 'text',
|
|
456
|
+
result: null,
|
|
457
|
+
startedAt: null,
|
|
458
|
+
completedAt: null,
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
} else if (leader) {
|
|
462
|
+
// Only leader available, leader executes directly
|
|
463
|
+
nodes.push({
|
|
464
|
+
id: 'node_work_0',
|
|
465
|
+
title: 'Execute requirement',
|
|
466
|
+
description: `Directly execute requirement "${requirement.title}": ${requirement.description}`,
|
|
467
|
+
assigneeId: leader.id,
|
|
468
|
+
assigneeName: leader.name,
|
|
469
|
+
dependencies: [],
|
|
470
|
+
status: TaskNodeStatus.READY,
|
|
471
|
+
outputType: 'text',
|
|
472
|
+
result: null,
|
|
473
|
+
startedAt: null,
|
|
474
|
+
completedAt: null,
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
const workflow = {
|
|
479
|
+
nodes,
|
|
480
|
+
summary: `Rule-based workflow: ${nodes.length} tasks`,
|
|
481
|
+
};
|
|
482
|
+
|
|
483
|
+
requirement.workflow = workflow;
|
|
484
|
+
return workflow;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
/**
|
|
488
|
+
* Execute workflow by DAG dependency order
|
|
489
|
+
* Supports parallel + dependency serialization
|
|
490
|
+
*/
|
|
491
|
+
async executeWorkflow(requirement, department, performanceSystem) {
|
|
492
|
+
if (!requirement.workflow?.nodes?.length) {
|
|
493
|
+
// If workflow is empty, auto-create a simple fallback workflow
|
|
494
|
+
console.log('Workflow is empty, auto-creating fallback workflow...');
|
|
495
|
+
const members = department.getMembers();
|
|
496
|
+
if (members.length === 0) {
|
|
497
|
+
throw new Error('Department has no employees, cannot execute');
|
|
498
|
+
} this._fallbackWorkflow(requirement, members);
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
requirement.status = RequirementStatus.IN_PROGRESS;
|
|
502
|
+
requirement.startedAt = new Date();
|
|
503
|
+
requirement.updateLiveStatus({
|
|
504
|
+
currentAction: 'Workflow execution started',
|
|
505
|
+
toolCallsInProgress: [],
|
|
506
|
+
recentFileChanges: [],
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
requirement.addGroupMessage(
|
|
510
|
+
{ name: 'System', role: 'system' },
|
|
511
|
+
`🚀 Requirement "${requirement.title}" execution started!`,
|
|
512
|
+
'system', null, { auto: true }
|
|
513
|
+
);
|
|
514
|
+
|
|
515
|
+
// === Set up message bus listener for real-time agent-to-agent communication ===
|
|
516
|
+
const messageBus = department.company?.messageBus || null;
|
|
517
|
+
const messageHandler = async (message) => {
|
|
518
|
+
// When an agent sends a message via send_message tool,
|
|
519
|
+
// post it to the group chat with @mention, then let GroupChatLoop
|
|
520
|
+
// handle the response through the normal flow/monologue mechanism.
|
|
521
|
+
// This ensures all replies go through the heartflow thinking process.
|
|
522
|
+
const receiverAgent = department.agents.get(message.to);
|
|
523
|
+
const senderAgent = department.agents.get(message.from);
|
|
524
|
+
if (!receiverAgent || !senderAgent) return;
|
|
525
|
+
|
|
526
|
+
// Show the sent message in group chat with @mention
|
|
527
|
+
const groupMsg = `@[${receiverAgent.id}] ${message.content}`;
|
|
528
|
+
requirement.addGroupMessage(senderAgent, groupMsg, 'message');
|
|
529
|
+
this._recordAgentChat(senderAgent, receiverAgent, message.content);
|
|
530
|
+
|
|
531
|
+
// Trigger the receiver's GroupChatLoop to process via heartflow
|
|
532
|
+
// instead of auto-replying directly (bypassing flow thinking)
|
|
533
|
+
try {
|
|
534
|
+
const { groupChatLoop } = await import('./organization/group-chat-loop.js');
|
|
535
|
+
groupChatLoop.triggerImmediate(receiverAgent.id, requirement.id, {
|
|
536
|
+
content: groupMsg,
|
|
537
|
+
from: senderAgent,
|
|
538
|
+
}).catch(() => {});
|
|
539
|
+
} catch (e) {
|
|
540
|
+
// Non-blocking
|
|
541
|
+
}
|
|
542
|
+
};
|
|
543
|
+
|
|
544
|
+
if (messageBus) {
|
|
545
|
+
messageBus.on('message', messageHandler);
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
const nodes = requirement.workflow.nodes;
|
|
549
|
+
const completed = new Set();
|
|
550
|
+
const failed = new Set();
|
|
551
|
+
const allResults = [];
|
|
552
|
+
|
|
553
|
+
// Loop until all nodes are completed or no further progress possible
|
|
554
|
+
while (completed.size + failed.size < nodes.length) {
|
|
555
|
+
// Find executable nodes (all dependencies completed + status is READY/WAITING)
|
|
556
|
+
const readyNodes = nodes.filter(n =>
|
|
557
|
+
n.status !== TaskNodeStatus.COMPLETED &&
|
|
558
|
+
n.status !== TaskNodeStatus.FAILED &&
|
|
559
|
+
n.status !== TaskNodeStatus.RUNNING &&
|
|
560
|
+
n.status !== TaskNodeStatus.REVIEWING &&
|
|
561
|
+
n.status !== TaskNodeStatus.REVISION &&
|
|
562
|
+
n.dependencies.every(d => completed.has(d))
|
|
563
|
+
);
|
|
564
|
+
|
|
565
|
+
if (readyNodes.length === 0) {
|
|
566
|
+
// No executable nodes left (could be circular dependency or all failed)
|
|
567
|
+
break;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
// Execute all ready nodes in parallel
|
|
571
|
+
// Build parallel context: let agents know who else is working in parallel
|
|
572
|
+
const parallelInfo = readyNodes.length > 1
|
|
573
|
+
? readyNodes.map(n => {
|
|
574
|
+
const a = department.agents.get(n.assigneeId);
|
|
575
|
+
return a ? `- ${a.name} (${a.role}): "${n.title}"` : null;
|
|
576
|
+
}).filter(Boolean).join('\n')
|
|
577
|
+
: null;
|
|
578
|
+
|
|
579
|
+
const promises = readyNodes.map(async (node) => {
|
|
580
|
+
node.status = TaskNodeStatus.RUNNING;
|
|
581
|
+
node.startedAt = new Date();
|
|
582
|
+
|
|
583
|
+
const agent = department.agents.get(node.assigneeId);
|
|
584
|
+
if (!agent) {
|
|
585
|
+
node.status = TaskNodeStatus.FAILED;
|
|
586
|
+
node.result = { error: 'Assignee not found' };
|
|
587
|
+
failed.add(node.id);
|
|
588
|
+
requirement.addGroupMessage(
|
|
589
|
+
{ name: 'System' },
|
|
590
|
+
`❌ Task "${node.title}" failed: Assignee not found`,
|
|
591
|
+
'system', null, { auto: true }
|
|
592
|
+
);
|
|
593
|
+
return null;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
// Update live status
|
|
597
|
+
requirement.updateLiveStatus({
|
|
598
|
+
currentNodeId: node.id,
|
|
599
|
+
currentNodeTitle: node.title,
|
|
600
|
+
currentAgent: agent.name,
|
|
601
|
+
currentAction: `${agent.name} is preparing to execute "${node.title}"`,
|
|
602
|
+
toolCallsInProgress: [],
|
|
603
|
+
});
|
|
604
|
+
|
|
605
|
+
// Group chat notification: starting work
|
|
606
|
+
if (parallelInfo && readyNodes.length > 1) {
|
|
607
|
+
// Parallel mode: announce teamwork
|
|
608
|
+
const myParallel = readyNodes
|
|
609
|
+
.filter(n => n.id !== node.id)
|
|
610
|
+
.map(n => {
|
|
611
|
+
const a = department.agents.get(n.assigneeId);
|
|
612
|
+
return a ? `@[${a.id}]` : null;
|
|
613
|
+
}).filter(Boolean);
|
|
614
|
+
if (myParallel.length > 0) {
|
|
615
|
+
requirement.addGroupMessage(
|
|
616
|
+
agent,
|
|
617
|
+
`🔨 Starting "${node.title}"! Working in parallel with ${myParallel.join(', ')} — let's sync up if needed! 💪`,
|
|
618
|
+
'message', null, { auto: true }
|
|
619
|
+
);
|
|
620
|
+
} else {
|
|
621
|
+
requirement.addGroupMessage(
|
|
622
|
+
agent,
|
|
623
|
+
`🔨 Starting to work on "${node.title}"!`,
|
|
624
|
+
'message', null, { auto: true }
|
|
625
|
+
);
|
|
626
|
+
}
|
|
627
|
+
} else {
|
|
628
|
+
requirement.addGroupMessage(
|
|
629
|
+
agent,
|
|
630
|
+
`🔨 Starting to work on "${node.title}"!`,
|
|
631
|
+
'message', null, { auto: true }
|
|
632
|
+
);
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
try {
|
|
636
|
+
// Collect dependency node outputs as context (including file deliverables)
|
|
637
|
+
const depContext = node.dependencies
|
|
638
|
+
.map(d => nodes.find(n => n.id === d))
|
|
639
|
+
.filter(Boolean)
|
|
640
|
+
.map(d => {
|
|
641
|
+
const output = d.result?.output || '(no output)';
|
|
642
|
+
// Extract files written by upstream task (only include files that still exist)
|
|
643
|
+
const depWsPath = department.workspacePath;
|
|
644
|
+
const fileWriteTools = new Set(['file_write', 'file_append', 'file_patch']);
|
|
645
|
+
const upstreamFiles = (d.result?.toolResults || [])
|
|
646
|
+
.filter(t => fileWriteTools.has(t.tool))
|
|
647
|
+
.map(t => t.args?.path || t.args?.filePath || t.args?.file_path || '')
|
|
648
|
+
.filter(f => f && (!depWsPath || existsSync(path.join(depWsPath, f))));
|
|
649
|
+
const fileList = upstreamFiles.length > 0
|
|
650
|
+
? `\n📁 Files delivered:\n${upstreamFiles.map(f => ` - ${f}`).join('\n')}\nYou can use file_read to review these files.`
|
|
651
|
+
: '';
|
|
652
|
+
return `[${d.assigneeName}'s output - ${d.title}]\n${output}${fileList}`;
|
|
653
|
+
})
|
|
654
|
+
.join('\n\n');
|
|
655
|
+
|
|
656
|
+
// Build colleague info for collaboration context
|
|
657
|
+
const colleagues = Array.from(department.agents.values())
|
|
658
|
+
.filter(a => a.id !== agent.id)
|
|
659
|
+
.map(a => `- ${a.name} (${a.role}), ID: ${a.id}`)
|
|
660
|
+
.join('\n');
|
|
661
|
+
|
|
662
|
+
// Build parallel context: who is working at the same time?
|
|
663
|
+
let parallelContext = '';
|
|
664
|
+
if (parallelInfo && readyNodes.length > 1) {
|
|
665
|
+
const otherParallel = readyNodes
|
|
666
|
+
.filter(n => n.id !== node.id)
|
|
667
|
+
.map(n => {
|
|
668
|
+
const a = department.agents.get(n.assigneeId);
|
|
669
|
+
return a ? `- ${a.name} (${a.role}) is working on "${n.title}" right now` : null;
|
|
670
|
+
}).filter(Boolean).join('\n');
|
|
671
|
+
if (otherParallel) {
|
|
672
|
+
parallelContext = `\n\n**Working in parallel with you right now:**\n${otherParallel}\nFeel free to use send_message to coordinate with them, share progress, or ask questions!`;
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
const task = {
|
|
677
|
+
title: node.title,
|
|
678
|
+
description: node.description,
|
|
679
|
+
context: (depContext ? `Here are the outputs from preceding tasks for your reference:\n\n${depContext}\n\n` : '')
|
|
680
|
+
+ (colleagues ? `Your colleagues in this department (you can send_message to them):\n${colleagues}` : '')
|
|
681
|
+
+ parallelContext,
|
|
682
|
+
requirements: `This is part of requirement "${requirement.title}". Requirement description: ${requirement.description}`,
|
|
683
|
+
};
|
|
684
|
+
|
|
685
|
+
// Update live status: starting LLM call
|
|
686
|
+
requirement.updateLiveStatus({
|
|
687
|
+
currentAction: `${agent.name} is typing..."${node.title}"`,
|
|
688
|
+
});
|
|
689
|
+
requirement.addGroupMessage(
|
|
690
|
+
agent,
|
|
691
|
+
`⌨️ Typing... planning how to complete this task`,
|
|
692
|
+
'tool_call'
|
|
693
|
+
);
|
|
694
|
+
|
|
695
|
+
// Take workspace snapshot before CLI execution for file change detection
|
|
696
|
+
const wsPath = department.workspacePath;
|
|
697
|
+
const isCLIAgent = !!agent.cliBackend;
|
|
698
|
+
let snapshotBefore = null;
|
|
699
|
+
if (isCLIAgent && wsPath) {
|
|
700
|
+
try {
|
|
701
|
+
const wsm = new WorkspaceManager();
|
|
702
|
+
snapshotBefore = await wsm.takeSnapshot(wsPath);
|
|
703
|
+
} catch { /* ignore snapshot errors */ }
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
const result = await agent.executeTask(task, {
|
|
707
|
+
onToolCall: ({ tool, args, status, success, error: toolErr }) => {
|
|
708
|
+
// Update requirement's liveStatus in real-time
|
|
709
|
+
if (status === 'start') {
|
|
710
|
+
// CLI progress heartbeat — special handling
|
|
711
|
+
if (tool === 'cli_progress') {
|
|
712
|
+
const elapsed = args?.elapsed || 0;
|
|
713
|
+
const backend = args?.backend || 'CLI';
|
|
714
|
+
requirement.updateLiveStatus({
|
|
715
|
+
currentAction: `${agent.name} is working via ${backend}... (${elapsed}s elapsed)`,
|
|
716
|
+
});
|
|
717
|
+
requirement.addGroupMessage(agent, `🖥️ Still working via ${backend}... (${elapsed}s elapsed)`, 'tool_call');
|
|
718
|
+
return;
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
requirement.updateLiveStatus({
|
|
722
|
+
currentAction: `${agent.name} is calling tool ${tool}`,
|
|
723
|
+
toolCallsInProgress: [...(requirement.liveStatus.toolCallsInProgress || []), tool],
|
|
724
|
+
});
|
|
725
|
+
// Real-time group chat: calling tool
|
|
726
|
+
if (tool === 'file_write') {
|
|
727
|
+
const filePath = args?.path || args?.filePath || args?.file_path || '';
|
|
728
|
+
requirement.addGroupMessage(agent, `📝 Writing file: ${filePath}`, 'tool_call');
|
|
729
|
+
requirement.addFileChange(agent.name, filePath, 'write');
|
|
730
|
+
} else if (tool === 'file_append') {
|
|
731
|
+
const filePath = args?.path || args?.filePath || args?.file_path || '';
|
|
732
|
+
requirement.addGroupMessage(agent, `📝 Appending to file: ${filePath}`, 'tool_call');
|
|
733
|
+
requirement.addFileChange(agent.name, filePath, 'write');
|
|
734
|
+
} else if (tool === 'file_patch') {
|
|
735
|
+
const filePath = args?.path || args?.filePath || args?.file_path || '';
|
|
736
|
+
requirement.addGroupMessage(agent, `📝 Patching file: ${filePath}`, 'tool_call');
|
|
737
|
+
requirement.addFileChange(agent.name, filePath, 'write');
|
|
738
|
+
} else if (tool === 'file_read') {
|
|
739
|
+
requirement.addGroupMessage(agent, `📄 Reading file: ${args?.path || args?.filePath || args?.file_path || ''}`, 'tool_call');
|
|
740
|
+
} else if (tool === 'shell_exec') {
|
|
741
|
+
requirement.addGroupMessage(agent, `⌨️ Executing command: ${(args?.command || '').slice(0, 80)}`, 'tool_call');
|
|
742
|
+
} else if (tool === 'send_message') {
|
|
743
|
+
requirement.addGroupMessage(agent, `💬 Sending message to colleague`, 'tool_call');
|
|
744
|
+
} else {
|
|
745
|
+
requirement.addGroupMessage(agent, `🔧 Using tool: ${tool}`, 'tool_call');
|
|
746
|
+
}
|
|
747
|
+
} else if (status === 'done') {
|
|
748
|
+
// CLI complete — special handling
|
|
749
|
+
if (tool === 'cli_complete') {
|
|
750
|
+
const backend = args?.backend || 'CLI';
|
|
751
|
+
const exitCode = args?.exitCode;
|
|
752
|
+
requirement.updateLiveStatus({
|
|
753
|
+
currentAction: `${agent.name} completed work via ${backend} (exit: ${exitCode})`,
|
|
754
|
+
toolCallsInProgress: [],
|
|
755
|
+
});
|
|
756
|
+
requirement.addGroupMessage(agent, `✅ ${backend} execution completed (exit code: ${exitCode})`, 'tool_call');
|
|
757
|
+
return;
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
requirement.updateLiveStatus({
|
|
761
|
+
currentAction: `${agent.name} completed tool call ${tool}`,
|
|
762
|
+
toolCallsInProgress: (requirement.liveStatus.toolCallsInProgress || []).filter(t => t !== tool),
|
|
763
|
+
});
|
|
764
|
+
} else if (status === 'error') {
|
|
765
|
+
requirement.addGroupMessage(agent, `⚠️ Tool ${tool} failed: ${toolErr}`, 'tool_call');
|
|
766
|
+
requirement.updateLiveStatus({
|
|
767
|
+
toolCallsInProgress: (requirement.liveStatus.toolCallsInProgress || []).filter(t => t !== tool),
|
|
768
|
+
});
|
|
769
|
+
}
|
|
770
|
+
},
|
|
771
|
+
onLLMCall: ({ iteration, maxIterations }) => {
|
|
772
|
+
requirement.updateLiveStatus({
|
|
773
|
+
currentAction: `${agent.name} is typing... (round ${iteration})`,
|
|
774
|
+
});
|
|
775
|
+
if (iteration > 1) {
|
|
776
|
+
requirement.addGroupMessage(agent, `🧠 Continuing to think and execute... (round ${iteration})`, 'tool_call');
|
|
777
|
+
}
|
|
778
|
+
},
|
|
779
|
+
});
|
|
780
|
+
|
|
781
|
+
// After CLI execution: detect file changes by comparing snapshots
|
|
782
|
+
if (snapshotBefore && wsPath) {
|
|
783
|
+
try {
|
|
784
|
+
const wsm = new WorkspaceManager();
|
|
785
|
+
const snapshotAfter = await wsm.takeSnapshot(wsPath);
|
|
786
|
+
const { created, modified } = wsm.diffSnapshots(snapshotBefore, snapshotAfter);
|
|
787
|
+
for (const fp of created) {
|
|
788
|
+
requirement.addFileChange(agent.name, fp, 'create');
|
|
789
|
+
}
|
|
790
|
+
for (const fp of modified) {
|
|
791
|
+
requirement.addFileChange(agent.name, fp, 'write');
|
|
792
|
+
}
|
|
793
|
+
if (created.length + modified.length > 0) {
|
|
794
|
+
console.log(` 📁 [CLI file detection] ${agent.name}: ${created.length} created, ${modified.length} modified`);
|
|
795
|
+
}
|
|
796
|
+
} catch (err) {
|
|
797
|
+
console.warn(` ⚠️ CLI file change detection failed:`, err.message);
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
node.status = TaskNodeStatus.COMPLETED;
|
|
802
|
+
node.completedAt = new Date();
|
|
803
|
+
node.result = result;
|
|
804
|
+
|
|
805
|
+
// === QUALITY CHECK: Warn if review/integration tasks didn't actually read files ===
|
|
806
|
+
const titleLower = (node.title || '').toLowerCase();
|
|
807
|
+
const isReviewLikeTask = titleLower.includes('review') || titleLower.includes('审') ||
|
|
808
|
+
titleLower.includes('integrat') || titleLower.includes('整合') ||
|
|
809
|
+
titleLower.includes('check') || titleLower.includes('检查') ||
|
|
810
|
+
titleLower.includes('final') || titleLower.includes('最终');
|
|
811
|
+
const usedFileRead = (result.toolResults || []).some(t => t.tool === 'file_read');
|
|
812
|
+
const hasWrittenFiles = (result.toolResults || []).some(t =>
|
|
813
|
+
t.tool === 'file_write' || t.tool === 'file_append' || t.tool === 'file_patch'
|
|
814
|
+
);
|
|
815
|
+
if (isReviewLikeTask && !usedFileRead && !hasWrittenFiles && result.duration < 15000) {
|
|
816
|
+
requirement.addGroupMessage(
|
|
817
|
+
{ name: 'System', role: 'system' },
|
|
818
|
+
`⚠️ Task "${node.title}" appears to be a review/integration task but completed in ${Math.round(result.duration / 1000)}s without reading any files. The output may be superficial.`,
|
|
819
|
+
'system', null, { auto: true }
|
|
820
|
+
);
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
// === REVIEW GATE: If reviewer assigned, trigger strict review loop ===
|
|
824
|
+
if (node.reviewerId && node.reviewerId !== node.assigneeId) {
|
|
825
|
+
const reviewer = department.agents.get(node.reviewerId);
|
|
826
|
+
if (reviewer) {
|
|
827
|
+
node.status = TaskNodeStatus.REVIEWING;
|
|
828
|
+
requirement.addGroupMessage(
|
|
829
|
+
{ name: 'System', role: 'system' },
|
|
830
|
+
`🔍 Task "${node.title}" entering review phase`,
|
|
831
|
+
'system', null, { auto: true }
|
|
832
|
+
);
|
|
833
|
+
requirement.addGroupMessage(
|
|
834
|
+
reviewer,
|
|
835
|
+
`🔍 @[${agent.id}] I'm now reviewing your work on "${node.title}". Let me take a careful look...`,
|
|
836
|
+
'message', null, { auto: true }
|
|
837
|
+
);
|
|
838
|
+
|
|
839
|
+
let currentOutput = result.output;
|
|
840
|
+
let approved = false;
|
|
841
|
+
const maxRounds = node.maxReviewRounds || 10;
|
|
842
|
+
|
|
843
|
+
while (!approved && node.reviewRounds < maxRounds) {
|
|
844
|
+
node.reviewRounds++;
|
|
845
|
+
|
|
846
|
+
// Reviewer performs strict review
|
|
847
|
+
requirement.updateLiveStatus({
|
|
848
|
+
currentNodeId: node.id,
|
|
849
|
+
currentNodeTitle: `Review: ${node.title}`,
|
|
850
|
+
currentAgent: reviewer.name,
|
|
851
|
+
currentAction: `${reviewer.name} is reviewing "${node.title}" (round ${node.reviewRounds})`,
|
|
852
|
+
});
|
|
853
|
+
|
|
854
|
+
const reviewResult = await this._strictReview(
|
|
855
|
+
reviewer, agent, node, currentOutput, requirement, node.reviewRounds, department
|
|
856
|
+
);
|
|
857
|
+
|
|
858
|
+
if (reviewResult.approved) {
|
|
859
|
+
approved = true;
|
|
860
|
+
requirement.addGroupMessage(
|
|
861
|
+
reviewer,
|
|
862
|
+
`✅ @[${agent.id}] Review APPROVED for "${node.title}"${node.reviewRounds > 1 ? ` (after ${node.reviewRounds} rounds)` : ''}! ${reviewResult.comment || 'Good work!'}`,
|
|
863
|
+
'message', null, { auto: true }
|
|
864
|
+
);
|
|
865
|
+
this._recordAgentChat(reviewer, agent, `✅ Review APPROVED: ${reviewResult.comment || 'Good work!'}`);
|
|
866
|
+
} else {
|
|
867
|
+
// Review rejected — enter negotiation phase
|
|
868
|
+
node.status = TaskNodeStatus.REVISION;
|
|
869
|
+
requirement.addGroupMessage(
|
|
870
|
+
reviewer,
|
|
871
|
+
`❌ @[${agent.id}] Review REJECTED for "${node.title}" (round ${node.reviewRounds}/${maxRounds}):\n${reviewResult.feedback}`,
|
|
872
|
+
'message', null, { auto: true }
|
|
873
|
+
);
|
|
874
|
+
this._recordAgentChat(reviewer, agent, `❌ Review REJECTED (round ${node.reviewRounds}): ${reviewResult.feedback}`);
|
|
875
|
+
|
|
876
|
+
if (node.reviewRounds >= maxRounds) {
|
|
877
|
+
// Max rounds reached, force approve with warning
|
|
878
|
+
approved = true;
|
|
879
|
+
requirement.addGroupMessage(
|
|
880
|
+
{ name: 'System', role: 'system' },
|
|
881
|
+
`⚠️ Review for "${node.title}" reached max rounds (${maxRounds}). Force-proceeding with latest revision.`,
|
|
882
|
+
'system', null, { auto: true }
|
|
883
|
+
);
|
|
884
|
+
} else {
|
|
885
|
+
// === Negotiation phase: reviewee can accept or contest the feedback ===
|
|
886
|
+
const rebuttalResult = await this._assigneeRebuttal(
|
|
887
|
+
agent, reviewer, node, currentOutput, reviewResult.feedback, requirement, node.reviewRounds
|
|
888
|
+
);
|
|
889
|
+
|
|
890
|
+
if (rebuttalResult.accept) {
|
|
891
|
+
// Reviewee accepts feedback, making revisions
|
|
892
|
+
node.status = TaskNodeStatus.RUNNING;
|
|
893
|
+
requirement.updateLiveStatus({
|
|
894
|
+
currentNodeId: node.id,
|
|
895
|
+
currentNodeTitle: `Revision: ${node.title}`,
|
|
896
|
+
currentAgent: agent.name,
|
|
897
|
+
currentAction: `${agent.name} is revising "${node.title}" based on review feedback (round ${node.reviewRounds})`,
|
|
898
|
+
});
|
|
899
|
+
requirement.addGroupMessage(
|
|
900
|
+
agent,
|
|
901
|
+
`🔄 @[${reviewer.id}] ${rebuttalResult.message || `Got it, revising "${node.title}" based on your feedback...`}`,
|
|
902
|
+
'message', null, { auto: true }
|
|
903
|
+
);
|
|
904
|
+
|
|
905
|
+
try {
|
|
906
|
+
const revisionResult = await this._executeRevision(
|
|
907
|
+
agent, node, currentOutput, reviewResult.feedback, requirement, {
|
|
908
|
+
onToolCall: ({ tool, args, status, success, error: toolErr }) => {
|
|
909
|
+
if (status === 'start') {
|
|
910
|
+
requirement.updateLiveStatus({
|
|
911
|
+
currentAction: `${agent.name} (revision) calling ${tool}`,
|
|
912
|
+
toolCallsInProgress: [...(requirement.liveStatus.toolCallsInProgress || []), tool],
|
|
913
|
+
});
|
|
914
|
+
if (tool === 'file_write' || tool === 'file_append' || tool === 'file_patch') {
|
|
915
|
+
const filePath = args?.path || args?.filePath || args?.file_path || '';
|
|
916
|
+
requirement.addGroupMessage(agent, `📝 [Revision] Writing file: ${filePath}`, 'tool_call');
|
|
917
|
+
requirement.addFileChange(agent.name, filePath, 'write');
|
|
918
|
+
}
|
|
919
|
+
} else if (status === 'done') {
|
|
920
|
+
requirement.updateLiveStatus({
|
|
921
|
+
toolCallsInProgress: (requirement.liveStatus.toolCallsInProgress || []).filter(t => t !== tool),
|
|
922
|
+
});
|
|
923
|
+
}
|
|
924
|
+
},
|
|
925
|
+
onLLMCall: ({ iteration }) => {
|
|
926
|
+
requirement.updateLiveStatus({
|
|
927
|
+
currentAction: `${agent.name} is revising... (iteration ${iteration})`,
|
|
928
|
+
});
|
|
929
|
+
},
|
|
930
|
+
}
|
|
931
|
+
);
|
|
932
|
+
currentOutput = revisionResult.output || currentOutput;
|
|
933
|
+
} catch (revisionErr) {
|
|
934
|
+
console.error(` ❌ Revision failed for "${node.title}":`, revisionErr.message);
|
|
935
|
+
requirement.addGroupMessage(
|
|
936
|
+
agent,
|
|
937
|
+
`⚠️ Revision attempt failed: ${revisionErr.message}. Proceeding with previous output.`,
|
|
938
|
+
'message', null, { auto: true }
|
|
939
|
+
);
|
|
940
|
+
}
|
|
941
|
+
} else {
|
|
942
|
+
// Reviewee contests! Entering confrontation phase
|
|
943
|
+
requirement.addGroupMessage(
|
|
944
|
+
agent,
|
|
945
|
+
`💬 @[${reviewer.id}] ${rebuttalResult.message}`,
|
|
946
|
+
'message', null, { auto: true }
|
|
947
|
+
);
|
|
948
|
+
this._recordAgentChat(agent, reviewer, `💬 Rebuttal: ${rebuttalResult.message}`);
|
|
949
|
+
|
|
950
|
+
// Reviewer re-evaluates
|
|
951
|
+
const reEvalResult = await this._reviewerReEvaluate(
|
|
952
|
+
reviewer, agent, node, currentOutput, reviewResult.feedback, rebuttalResult.message, requirement, node.reviewRounds
|
|
953
|
+
);
|
|
954
|
+
|
|
955
|
+
if (reEvalResult.convinced) {
|
|
956
|
+
// Reviewer was persuaded!
|
|
957
|
+
approved = true;
|
|
958
|
+
requirement.addGroupMessage(
|
|
959
|
+
reviewer,
|
|
960
|
+
`✅ @[${agent.id}] ${reEvalResult.message || `Fair point! I'll approve "${node.title}".`}`,
|
|
961
|
+
'message', null, { auto: true }
|
|
962
|
+
);
|
|
963
|
+
this._recordAgentChat(reviewer, agent, `✅ Convinced by rebuttal, approved: ${reEvalResult.message}`);
|
|
964
|
+
} else {
|
|
965
|
+
// Reviewer stands firm
|
|
966
|
+
requirement.addGroupMessage(
|
|
967
|
+
reviewer,
|
|
968
|
+
`🤔 @[${agent.id}] ${reEvalResult.message || `I understand your point, but I still think the issues need to be addressed.`}`,
|
|
969
|
+
'message', null, { auto: true }
|
|
970
|
+
);
|
|
971
|
+
this._recordAgentChat(reviewer, agent, `🤔 Not convinced: ${reEvalResult.message}`);
|
|
972
|
+
|
|
973
|
+
// Reviewee ultimately must revise
|
|
974
|
+
node.status = TaskNodeStatus.RUNNING;
|
|
975
|
+
requirement.updateLiveStatus({
|
|
976
|
+
currentNodeId: node.id,
|
|
977
|
+
currentNodeTitle: `Revision: ${node.title}`,
|
|
978
|
+
currentAgent: agent.name,
|
|
979
|
+
currentAction: `${agent.name} is revising "${node.title}" after discussion (round ${node.reviewRounds})`,
|
|
980
|
+
});
|
|
981
|
+
requirement.addGroupMessage(
|
|
982
|
+
agent,
|
|
983
|
+
`🔄 @[${reviewer.id}] Alright, I'll revise "${node.title}" incorporating your feedback.`,
|
|
984
|
+
'message', null, { auto: true }
|
|
985
|
+
);
|
|
986
|
+
|
|
987
|
+
try {
|
|
988
|
+
const revisionResult = await this._executeRevision(
|
|
989
|
+
agent, node, currentOutput, `${reviewResult.feedback}\n\n[Discussion context] Assignee argued: "${rebuttalResult.message}" but reviewer insisted: "${reEvalResult.message}"`, requirement, {
|
|
990
|
+
onToolCall: ({ tool, args, status }) => {
|
|
991
|
+
if (status === 'start') {
|
|
992
|
+
requirement.updateLiveStatus({
|
|
993
|
+
currentAction: `${agent.name} (revision) calling ${tool}`,
|
|
994
|
+
toolCallsInProgress: [...(requirement.liveStatus.toolCallsInProgress || []), tool],
|
|
995
|
+
});
|
|
996
|
+
if (tool === 'file_write' || tool === 'file_append' || tool === 'file_patch') {
|
|
997
|
+
const filePath = args?.path || args?.filePath || args?.file_path || '';
|
|
998
|
+
requirement.addGroupMessage(agent, `📝 [Revision] Writing file: ${filePath}`, 'tool_call');
|
|
999
|
+
requirement.addFileChange(agent.name, filePath, 'write');
|
|
1000
|
+
}
|
|
1001
|
+
} else if (status === 'done') {
|
|
1002
|
+
requirement.updateLiveStatus({
|
|
1003
|
+
toolCallsInProgress: (requirement.liveStatus.toolCallsInProgress || []).filter(t => t !== tool),
|
|
1004
|
+
});
|
|
1005
|
+
}
|
|
1006
|
+
},
|
|
1007
|
+
onLLMCall: ({ iteration }) => {
|
|
1008
|
+
requirement.updateLiveStatus({
|
|
1009
|
+
currentAction: `${agent.name} is revising after discussion... (iteration ${iteration})`,
|
|
1010
|
+
});
|
|
1011
|
+
},
|
|
1012
|
+
}
|
|
1013
|
+
);
|
|
1014
|
+
currentOutput = revisionResult.output || currentOutput;
|
|
1015
|
+
} catch (revisionErr) {
|
|
1016
|
+
console.error(` ❌ Revision after discussion failed for "${node.title}":`, revisionErr.message);
|
|
1017
|
+
requirement.addGroupMessage(
|
|
1018
|
+
agent,
|
|
1019
|
+
`⚠️ Revision attempt failed: ${revisionErr.message}. Proceeding with previous output.`,
|
|
1020
|
+
'message', null, { auto: true }
|
|
1021
|
+
);
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
// After negotiation/revision, notify reviewer to re-review (if not approved during negotiation)
|
|
1027
|
+
if (!approved) {
|
|
1028
|
+
requirement.addGroupMessage(
|
|
1029
|
+
agent,
|
|
1030
|
+
`📝 @[${reviewer.id}] Revision complete for "${node.title}", please review again.`,
|
|
1031
|
+
'message', null, { auto: true }
|
|
1032
|
+
);
|
|
1033
|
+
this._recordAgentChat(agent, reviewer, `Revision complete, please review again.`);
|
|
1034
|
+
|
|
1035
|
+
// Back to REVIEWING for next loop iteration
|
|
1036
|
+
node.status = TaskNodeStatus.REVIEWING;
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
// Final status
|
|
1043
|
+
node.status = TaskNodeStatus.COMPLETED;
|
|
1044
|
+
node.completedAt = new Date();
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
// === END REVIEW GATE ===
|
|
1048
|
+
|
|
1049
|
+
completed.add(node.id);
|
|
1050
|
+
|
|
1051
|
+
// Record output (use node.result which may have been updated by revision)
|
|
1052
|
+
const finalResult = node.result || result;
|
|
1053
|
+
if (finalResult.output) {
|
|
1054
|
+
// Determine output type
|
|
1055
|
+
const outputType = this._detectOutputType(finalResult);
|
|
1056
|
+
// Clean LLM tool-call markup from output before storing
|
|
1057
|
+
const cleanedOutputForStore = this._cleanLLMOutput(finalResult.output) || finalResult.output;
|
|
1058
|
+
requirement.addOutput(
|
|
1059
|
+
agent.id, agent.name, agent.role,
|
|
1060
|
+
outputType, cleanedOutputForStore,
|
|
1061
|
+
{ toolResults: finalResult.toolResults, duration: finalResult.duration }
|
|
1062
|
+
);
|
|
1063
|
+
} else if (finalResult.toolResults?.length > 0) {
|
|
1064
|
+
// Even if output text is empty, if tools were used (e.g. file_write),
|
|
1065
|
+
// still record an output entry so it shows in the Outputs tab
|
|
1066
|
+
const fileWriteToolsForOutput = new Set(['file_write', 'file_append', 'file_patch']);
|
|
1067
|
+
const fileWrites = (finalResult.toolResults || []).filter(t => fileWriteToolsForOutput.has(t.tool));
|
|
1068
|
+
if (fileWrites.length > 0) {
|
|
1069
|
+
const filePaths = fileWrites.map(t => t.args?.path || t.args?.filePath || t.args?.file_path || 'unknown').join(', ');
|
|
1070
|
+
requirement.addOutput(
|
|
1071
|
+
agent.id, agent.name, agent.role,
|
|
1072
|
+
'code', `[Files written] ${filePaths}`,
|
|
1073
|
+
{ toolResults: finalResult.toolResults, duration: finalResult.duration }
|
|
1074
|
+
);
|
|
1075
|
+
} else {
|
|
1076
|
+
const toolNames = finalResult.toolResults.map(t => t.tool).join(', ');
|
|
1077
|
+
requirement.addOutput(
|
|
1078
|
+
agent.id, agent.name, agent.role,
|
|
1079
|
+
'text', `[Tools used] ${toolNames}`,
|
|
1080
|
+
{ toolResults: finalResult.toolResults, duration: finalResult.duration }
|
|
1081
|
+
);
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
// Group chat notification: completed — with file references for written files
|
|
1086
|
+
const duration = Math.round((finalResult.duration || 0) / 1000);
|
|
1087
|
+
const fileWriteTools = new Set(['file_write', 'file_append', 'file_patch']);
|
|
1088
|
+
const fileWrites = (finalResult.toolResults || []).filter(t => fileWriteTools.has(t.tool));
|
|
1089
|
+
const validFileRefs = fileWrites
|
|
1090
|
+
.map(t => {
|
|
1091
|
+
const fp = t.args?.path || t.args?.filePath || t.args?.file_path || '';
|
|
1092
|
+
// Skip empty paths (can happen when LLM sends malformed tool args)
|
|
1093
|
+
if (!fp) return null;
|
|
1094
|
+
// Only create clickable reference if file actually exists
|
|
1095
|
+
if (wsPath && !existsSync(path.join(wsPath, fp))) return null;
|
|
1096
|
+
const name = fp.split('/').pop() || fp;
|
|
1097
|
+
return `[[file:${requirement.departmentId}:${fp}|${name}]]`;
|
|
1098
|
+
})
|
|
1099
|
+
.filter(Boolean);
|
|
1100
|
+
const fileRefTags = validFileRefs.length > 0
|
|
1101
|
+
? '\n' + validFileRefs.join(' ')
|
|
1102
|
+
: '';
|
|
1103
|
+
requirement.addGroupMessage(
|
|
1104
|
+
agent,
|
|
1105
|
+
`✅ "${node.title}" completed! Took ${duration}s.${finalResult.toolResults?.length ? `\n🔧 Used ${finalResult.toolResults.length} tools` : ''}${node.reviewRounds > 0 ? `\n🔍 Passed review after ${node.reviewRounds} round(s)` : ''}${fileRefTags}`,
|
|
1106
|
+
'message', null, { auto: true }
|
|
1107
|
+
);
|
|
1108
|
+
|
|
1109
|
+
// Share output: prioritize file references over LLM text summary
|
|
1110
|
+
const hasFiles = validFileRefs.length > 0;
|
|
1111
|
+
if (!hasFiles && finalResult.output?.trim()) {
|
|
1112
|
+
// No files written → this is a text-only task, show the LLM text output
|
|
1113
|
+
const cleanedOutput = this._cleanLLMOutput(finalResult.output);
|
|
1114
|
+
if (cleanedOutput && cleanedOutput.length > 20) {
|
|
1115
|
+
const preview = cleanedOutput.length > 300
|
|
1116
|
+
? cleanedOutput.slice(0, 300) + '...\n(output truncated)'
|
|
1117
|
+
: cleanedOutput;
|
|
1118
|
+
requirement.addGroupMessage(
|
|
1119
|
+
agent,
|
|
1120
|
+
`📄 My output:\n${preview}`,
|
|
1121
|
+
'message', null, { auto: true }
|
|
1122
|
+
);
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
// If files were written, the ✅ completed message above already includes
|
|
1126
|
+
// clickable [[file:...]] references — no need to also dump LLM's text summary,
|
|
1127
|
+
// which is typically just a verbose description of what was already done.
|
|
1128
|
+
|
|
1129
|
+
// Agent-to-agent collaboration: notify downstream agents with @mention
|
|
1130
|
+
const downstreamNodes = nodes.filter(n =>
|
|
1131
|
+
n.dependencies.includes(node.id) &&
|
|
1132
|
+
n.status !== TaskNodeStatus.COMPLETED &&
|
|
1133
|
+
n.status !== TaskNodeStatus.FAILED
|
|
1134
|
+
);
|
|
1135
|
+
if (downstreamNodes.length > 0 && finalResult.output) {
|
|
1136
|
+
for (const downNode of downstreamNodes) {
|
|
1137
|
+
const downAgent = department.agents.get(downNode.assigneeId);
|
|
1138
|
+
if (downAgent && downAgent.id !== agent.id) {
|
|
1139
|
+
// Record in group chat with @mention + file references
|
|
1140
|
+
const nodeFileRefs = fileRefTags ? `\nFiles: ${fileRefTags.trim()}` : '';
|
|
1141
|
+
const mentionMsg = `@[${downAgent.id}] I've completed "${node.title}", the output is ready for your "${downNode.title}" task. Please review and let me know if anything needs adjustment!${nodeFileRefs}`;
|
|
1142
|
+
requirement.addGroupMessage(agent, mentionMsg, 'message', null, { auto: true });
|
|
1143
|
+
|
|
1144
|
+
// Persist to agent-to-agent chatStore
|
|
1145
|
+
this._recordAgentChat(agent, downAgent, mentionMsg);
|
|
1146
|
+
|
|
1147
|
+
// Trigger downstream agent's GroupChatLoop to process via heartflow
|
|
1148
|
+
// instead of auto-replying directly (bypassing flow thinking)
|
|
1149
|
+
try {
|
|
1150
|
+
const { groupChatLoop } = await import('./organization/group-chat-loop.js');
|
|
1151
|
+
groupChatLoop.triggerImmediate(downAgent.id, requirement.id, {
|
|
1152
|
+
content: mentionMsg,
|
|
1153
|
+
from: agent,
|
|
1154
|
+
}).catch(() => {});
|
|
1155
|
+
} catch (e) {
|
|
1156
|
+
// Non-blocking
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
// Also notify parallel peers that just completed (non-blocking peer sync)
|
|
1163
|
+
const justCompletedPeers = readyNodes
|
|
1164
|
+
.filter(n => n.id !== node.id && n.status === TaskNodeStatus.COMPLETED)
|
|
1165
|
+
.slice(0, 2); // Limit to avoid spam
|
|
1166
|
+
for (const peerNode of justCompletedPeers) {
|
|
1167
|
+
const peerAgent = department.agents.get(peerNode.assigneeId);
|
|
1168
|
+
if (peerAgent && peerAgent.id !== agent.id) {
|
|
1169
|
+
const syncMsg = `@[${peerAgent.id}] Just finished my part "${node.title}" ✅ — how's yours going?`;
|
|
1170
|
+
requirement.addGroupMessage(agent, syncMsg, 'message', null, { auto: true });
|
|
1171
|
+
this._recordAgentChat(agent, peerAgent, syncMsg);
|
|
1172
|
+
|
|
1173
|
+
// Trigger peer's GroupChatLoop to process via heartflow
|
|
1174
|
+
try {
|
|
1175
|
+
const { groupChatLoop } = await import('./organization/group-chat-loop.js');
|
|
1176
|
+
groupChatLoop.triggerImmediate(peerAgent.id, requirement.id, {
|
|
1177
|
+
content: syncMsg,
|
|
1178
|
+
from: agent,
|
|
1179
|
+
}).catch(() => {});
|
|
1180
|
+
} catch (e) {
|
|
1181
|
+
// Non-blocking
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
allResults.push(finalResult);
|
|
1187
|
+
return finalResult;
|
|
1188
|
+
} catch (err) {
|
|
1189
|
+
node.status = TaskNodeStatus.FAILED;
|
|
1190
|
+
node.completedAt = new Date();
|
|
1191
|
+
node.result = { error: err.message, success: false };
|
|
1192
|
+
failed.add(node.id);
|
|
1193
|
+
|
|
1194
|
+
requirement.addGroupMessage(
|
|
1195
|
+
agent,
|
|
1196
|
+
`❌ "${node.title}" failed: ${err.message}`,
|
|
1197
|
+
'message', null, { auto: true }
|
|
1198
|
+
);
|
|
1199
|
+
return null;
|
|
1200
|
+
}
|
|
1201
|
+
});
|
|
1202
|
+
|
|
1203
|
+
await Promise.all(promises);
|
|
1204
|
+
|
|
1205
|
+
// === Parallel Sync Point: agents that just finished in this batch exchange feedback ===
|
|
1206
|
+
const justFinished = readyNodes.filter(n => n.status === TaskNodeStatus.COMPLETED);
|
|
1207
|
+
if (justFinished.length > 1) {
|
|
1208
|
+
requirement.addGroupMessage(
|
|
1209
|
+
{ name: 'System', role: 'system' },
|
|
1210
|
+
`🤝 ${justFinished.length} tasks completed in parallel! Agents are exchanging feedback...`,
|
|
1211
|
+
'system', null, { auto: true }
|
|
1212
|
+
);
|
|
1213
|
+
|
|
1214
|
+
// Each agent gives brief feedback to one other agent's work (round-robin, non-blocking)
|
|
1215
|
+
const syncPromises = justFinished.map(async (node, idx) => {
|
|
1216
|
+
const nextNode = justFinished[(idx + 1) % justFinished.length];
|
|
1217
|
+
if (nextNode.id === node.id) return; // Only 1 node, skip
|
|
1218
|
+
|
|
1219
|
+
const reviewer = department.agents.get(node.assigneeId);
|
|
1220
|
+
const reviewee = department.agents.get(nextNode.assigneeId);
|
|
1221
|
+
if (!reviewer || !reviewee || reviewer.id === reviewee.id) return;
|
|
1222
|
+
|
|
1223
|
+
try {
|
|
1224
|
+
const feedback = await this._agentPeerReview(
|
|
1225
|
+
reviewer, reviewee, nextNode.title, nextNode.result?.output, requirement
|
|
1226
|
+
);
|
|
1227
|
+
if (feedback) {
|
|
1228
|
+
const feedbackMsg = `@[${reviewee.id}] ${feedback}`;
|
|
1229
|
+
requirement.addGroupMessage(reviewer, feedbackMsg, 'message', null, { auto: true });
|
|
1230
|
+
this._recordAgentChat(reviewer, reviewee, feedbackMsg);
|
|
1231
|
+
}
|
|
1232
|
+
} catch (e) {
|
|
1233
|
+
// Non-blocking
|
|
1234
|
+
}
|
|
1235
|
+
});
|
|
1236
|
+
// Don't await all — fire and forget for non-critical feedback
|
|
1237
|
+
Promise.all(syncPromises).catch(() => {});
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
// === Clean up message bus listener ===
|
|
1242
|
+
if (messageBus) {
|
|
1243
|
+
messageBus.off('message', messageHandler);
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
// Summary
|
|
1247
|
+
const successCount = completed.size;
|
|
1248
|
+
const totalCount = nodes.length;
|
|
1249
|
+
const totalDuration = allResults.reduce((s, r) => s + (r?.duration || 0), 0);
|
|
1250
|
+
|
|
1251
|
+
// Detect suspiciously fast completions (tasks that finished too quickly with no real output)
|
|
1252
|
+
const suspiciousNodes = nodes.filter(n => {
|
|
1253
|
+
if (n.status !== TaskNodeStatus.COMPLETED) return false;
|
|
1254
|
+
const dur = n.result?.duration || 0;
|
|
1255
|
+
const hasOutput = n.result?.output?.trim();
|
|
1256
|
+
const hasToolResults = n.result?.toolResults?.length > 0;
|
|
1257
|
+
// Flag nodes that completed in < 5 seconds with no tool usage and minimal output
|
|
1258
|
+
return dur < 5000 && !hasToolResults && (!hasOutput || hasOutput.length < 50);
|
|
1259
|
+
});
|
|
1260
|
+
|
|
1261
|
+
if (suspiciousNodes.length > 0) {
|
|
1262
|
+
const names = suspiciousNodes.map(n => `"${n.title}"`).join(', ');
|
|
1263
|
+
requirement.addGroupMessage(
|
|
1264
|
+
{ name: 'System', role: 'system' },
|
|
1265
|
+
`⚠️ Warning: ${suspiciousNodes.length} task(s) completed suspiciously fast with minimal output: ${names}. These may not have been executed thoroughly.`,
|
|
1266
|
+
'system', null, { auto: true }
|
|
1267
|
+
);
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
if (failed.size === totalCount) {
|
|
1271
|
+
// All tasks failed → mark as FAILED immediately
|
|
1272
|
+
requirement.status = RequirementStatus.FAILED;
|
|
1273
|
+
requirement.completedAt = new Date();
|
|
1274
|
+
requirement.updateLiveStatus({
|
|
1275
|
+
currentNodeId: null, currentNodeTitle: null, currentAgent: null,
|
|
1276
|
+
currentAction: 'Execution finished (all tasks failed)',
|
|
1277
|
+
toolCallsInProgress: [],
|
|
1278
|
+
});
|
|
1279
|
+
} else {
|
|
1280
|
+
// Tasks done → enter PENDING_APPROVAL, wait for Boss to review and confirm
|
|
1281
|
+
requirement.status = RequirementStatus.PENDING_APPROVAL;
|
|
1282
|
+
requirement.updateLiveStatus({
|
|
1283
|
+
currentNodeId: null, currentNodeTitle: null, currentAgent: null,
|
|
1284
|
+
currentAction: 'All tasks done — awaiting Boss approval',
|
|
1285
|
+
toolCallsInProgress: [],
|
|
1286
|
+
});
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
requirement.summary = {
|
|
1290
|
+
totalTasks: totalCount,
|
|
1291
|
+
successTasks: successCount,
|
|
1292
|
+
failedTasks: failed.size,
|
|
1293
|
+
totalDuration,
|
|
1294
|
+
outputs: requirement.outputs,
|
|
1295
|
+
suspiciousTasks: suspiciousNodes.length,
|
|
1296
|
+
};
|
|
1297
|
+
|
|
1298
|
+
if (requirement.status === RequirementStatus.FAILED) {
|
|
1299
|
+
requirement.addGroupMessage(
|
|
1300
|
+
{ name: 'System', role: 'system' },
|
|
1301
|
+
`❌ Requirement "${requirement.title}" failed!\n📊 ${successCount}/${totalCount} tasks succeeded, total duration ${Math.round(totalDuration / 1000)}s`,
|
|
1302
|
+
'system', null, { auto: true }
|
|
1303
|
+
);
|
|
1304
|
+
} else {
|
|
1305
|
+
requirement.addGroupMessage(
|
|
1306
|
+
{ name: 'System', role: 'system' },
|
|
1307
|
+
`📋 All tasks for "${requirement.title}" are done!\n📊 ${successCount}/${totalCount} tasks succeeded, total duration ${Math.round(totalDuration / 1000)}s${suspiciousNodes.length > 0 ? `\n⚠️ ${suspiciousNodes.length} task(s) may need manual review` : ''}\n\n⏳ **Pending your approval, Boss.** The requirement is NOT yet completed.\n💬 Please review the outputs, then reply in this chat:\n • "OK" / "通过" / "approved" → approve and finalize\n • Or send feedback to request changes`,
|
|
1308
|
+
'system', null, { auto: true }
|
|
1309
|
+
);
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
// Performance evaluation
|
|
1313
|
+
if (performanceSystem) {
|
|
1314
|
+
const leader = department.getLeader();
|
|
1315
|
+
for (const node of nodes) {
|
|
1316
|
+
if (node.status !== TaskNodeStatus.COMPLETED) continue;
|
|
1317
|
+
const agent = department.agents.get(node.assigneeId);
|
|
1318
|
+
if (!agent || !leader || leader.id === agent.id) continue;
|
|
1319
|
+
try {
|
|
1320
|
+
performanceSystem.autoEvaluate({
|
|
1321
|
+
agent,
|
|
1322
|
+
reviewer: leader,
|
|
1323
|
+
taskTitle: node.title,
|
|
1324
|
+
});
|
|
1325
|
+
} catch (e) { /* ignore */ }
|
|
1326
|
+
}
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
return requirement.summary;
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
/**
|
|
1333
|
+
* Agent collaboration reply: downstream agent reviews upstream output and responds
|
|
1334
|
+
* @param {Agent} responder - The agent responding
|
|
1335
|
+
* @param {Agent} sender - The agent who completed the task
|
|
1336
|
+
* @param {string} taskTitle - Completed task title
|
|
1337
|
+
* @param {string} output - Task output
|
|
1338
|
+
* @param {Requirement} requirement - Requirement context
|
|
1339
|
+
* @returns {Promise<string|null>} Reply content
|
|
1340
|
+
*/
|
|
1341
|
+
async _agentCollabReply(responder, sender, taskTitle, output, requirement) {
|
|
1342
|
+
if (!responder.canChat()) return null;
|
|
1343
|
+
|
|
1344
|
+
try {
|
|
1345
|
+
const p = responder.personality || {};
|
|
1346
|
+
const outputPreview = output.length > 500
|
|
1347
|
+
? output.slice(0, 500) + '\n...(truncated — if you need the full output, use file_read to read the workspace files)'
|
|
1348
|
+
: output;
|
|
1349
|
+
const response = await responder.chat([
|
|
1350
|
+
{
|
|
1351
|
+
role: 'system',
|
|
1352
|
+
content: `You are "${responder.name}", working as "${responder.role}".
|
|
1353
|
+
Your personality: ${p.trait || 'Professional'}. Speaking style: ${p.tone || 'Normal'}.
|
|
1354
|
+
You are collaborating with colleagues on requirement "${requirement.title}".
|
|
1355
|
+
A colleague just completed their task and shared the output with you.
|
|
1356
|
+
Please respond briefly (1-2 sentences) acknowledging their work, in your personality style.
|
|
1357
|
+
You can comment on the quality, ask a question, or just acknowledge. Keep it natural and brief.`
|
|
1358
|
+
},
|
|
1359
|
+
{
|
|
1360
|
+
role: 'user',
|
|
1361
|
+
content: `Your colleague ${sender.name} (${sender.role}) completed "${taskTitle}" and shared the output:\n\n${outputPreview}\n\nPlease respond briefly.`
|
|
1362
|
+
},
|
|
1363
|
+
], { temperature: 0.9, maxTokens: 128 });
|
|
1364
|
+
|
|
1365
|
+
return response.content?.trim() || null;
|
|
1366
|
+
} catch (e) {
|
|
1367
|
+
return null;
|
|
1368
|
+
}
|
|
1369
|
+
}
|
|
1370
|
+
|
|
1371
|
+
/**
|
|
1372
|
+
* Agent peer review: parallel peers review each other's work
|
|
1373
|
+
* More casual and constructive than upstream→downstream handoff
|
|
1374
|
+
*/
|
|
1375
|
+
async _agentPeerReview(reviewer, reviewee, taskTitle, output, requirement) {
|
|
1376
|
+
if (!reviewer.canChat()) return null;
|
|
1377
|
+
if (!output) return null;
|
|
1378
|
+
|
|
1379
|
+
try {
|
|
1380
|
+
const p = reviewer.personality || {};
|
|
1381
|
+
const outputPreview = output.length > 400
|
|
1382
|
+
? output.slice(0, 400) + '\n...(truncated — use file_read to view full content in workspace files)'
|
|
1383
|
+
: output;
|
|
1384
|
+
const response = await reviewer.chat([
|
|
1385
|
+
{
|
|
1386
|
+
role: 'system',
|
|
1387
|
+
content: `You are "${reviewer.name}", working as "${reviewer.role}".
|
|
1388
|
+
Your personality: ${p.trait || 'Professional'}. Speaking style: ${p.tone || 'Normal'}.
|
|
1389
|
+
You just completed your parallel task for requirement "${requirement.title}".
|
|
1390
|
+
A colleague who worked in parallel with you also just finished their task. Please give brief, constructive feedback (1-2 sentences) on their work.
|
|
1391
|
+
Be natural, in character, and collegial. You can praise, suggest improvements, or note synergies with your own work.`
|
|
1392
|
+
},
|
|
1393
|
+
{
|
|
1394
|
+
role: 'user',
|
|
1395
|
+
content: `Your colleague ${reviewee.name} (${reviewee.role}) completed "${taskTitle}" in parallel with you. Their output:\n\n${outputPreview}\n\nGive brief peer feedback.`
|
|
1396
|
+
},
|
|
1397
|
+
], { temperature: 0.9, maxTokens: 128 });
|
|
1398
|
+
|
|
1399
|
+
return response.content?.trim() || null;
|
|
1400
|
+
} catch (e) {
|
|
1401
|
+
return null;
|
|
1402
|
+
}
|
|
1403
|
+
}
|
|
1404
|
+
|
|
1405
|
+
/**
|
|
1406
|
+
* Strict review: reviewer carefully audits the assignee's work
|
|
1407
|
+
* Returns { approved: boolean, feedback: string, comment: string }
|
|
1408
|
+
*/
|
|
1409
|
+
async _strictReview(reviewer, assignee, node, output, requirement, round, department = null) {
|
|
1410
|
+
if (!reviewer.canChat()) {
|
|
1411
|
+
return { approved: true, feedback: '', comment: 'Reviewer unavailable, auto-approved.' };
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1414
|
+
try {
|
|
1415
|
+
const p = reviewer.personality || {};
|
|
1416
|
+
const outputContent = output?.length > 2000
|
|
1417
|
+
? output.slice(0, 2000) + '\n...(truncated)'
|
|
1418
|
+
: output;
|
|
1419
|
+
const reviewCriteria = node.reviewCriteria || 'Check for correctness, completeness, and quality.';
|
|
1420
|
+
|
|
1421
|
+
// Collect actual file contents written by the assignee so the reviewer can see real deliverables
|
|
1422
|
+
const fileWriteTools = new Set(['file_write', 'file_append', 'file_patch']);
|
|
1423
|
+
const writtenFiles = (node.result?.toolResults || [])
|
|
1424
|
+
.filter(t => fileWriteTools.has(t.tool))
|
|
1425
|
+
.map(t => t.args?.path || t.args?.filePath || t.args?.file_path || '')
|
|
1426
|
+
.filter(Boolean);
|
|
1427
|
+
|
|
1428
|
+
let fileContentsSection = '';
|
|
1429
|
+
if (writtenFiles.length > 0) {
|
|
1430
|
+
const wsPath = department?.workspacePath || reviewer.toolKit?.workspaceDir;
|
|
1431
|
+
if (wsPath) {
|
|
1432
|
+
const uniqueFiles = [...new Set(writtenFiles)];
|
|
1433
|
+
const fileSnippets = [];
|
|
1434
|
+
for (const fp of uniqueFiles.slice(0, 5)) {
|
|
1435
|
+
try {
|
|
1436
|
+
const fullPath = path.join(wsPath, fp);
|
|
1437
|
+
if (existsSync(fullPath)) {
|
|
1438
|
+
const content = readFileSync(fullPath, 'utf-8');
|
|
1439
|
+
const preview = content.length > 2000
|
|
1440
|
+
? content.slice(0, 2000) + '\n...(file truncated)'
|
|
1441
|
+
: content;
|
|
1442
|
+
fileSnippets.push(`--- File: ${fp} ---\n${preview}`);
|
|
1443
|
+
}
|
|
1444
|
+
} catch { /* skip unreadable files */ }
|
|
1445
|
+
}
|
|
1446
|
+
if (fileSnippets.length > 0) {
|
|
1447
|
+
fileContentsSection = `\n\n**Actual file contents delivered:**\n${fileSnippets.join('\n\n')}`;
|
|
1448
|
+
}
|
|
1449
|
+
}
|
|
1450
|
+
}
|
|
1451
|
+
|
|
1452
|
+
const response = await reviewer.chat([
|
|
1453
|
+
{
|
|
1454
|
+
role: 'system',
|
|
1455
|
+
content: `You are "${reviewer.name}", working as "${reviewer.role}".
|
|
1456
|
+
Your personality: ${p.trait || 'Professional'}. Speaking style: ${p.tone || 'Normal'}.
|
|
1457
|
+
|
|
1458
|
+
You are reviewing work for the requirement "${requirement.title}".
|
|
1459
|
+
|
|
1460
|
+
**Review Criteria for this task:**
|
|
1461
|
+
${reviewCriteria}
|
|
1462
|
+
|
|
1463
|
+
**Your review guidelines:**
|
|
1464
|
+
- Be fair and professional. Approve if the work reasonably meets the criteria, even if minor improvements are possible.
|
|
1465
|
+
- ONLY reject for SIGNIFICANT issues: critical bugs, missing core requirements, major quality gaps, or incomplete deliverables.
|
|
1466
|
+
- Do NOT reject for stylistic preferences, minor improvements, or nice-to-haves.
|
|
1467
|
+
- When rejecting, provide SPECIFIC, ACTIONABLE feedback explaining exactly what MUST be fixed.
|
|
1468
|
+
- When approving, briefly comment on what was done well. You may suggest minor improvements as non-blocking notes.
|
|
1469
|
+
- ${round === 1 ? 'This is the first review. Focus on whether core requirements are met.' : `This is review round ${round}. The assignee has revised based on your previous feedback. Check if the CRITICAL issues from your previous feedback were addressed. Minor issues that were not addressed can be accepted.`}
|
|
1470
|
+
- IMPORTANT: If the assignee has produced actual file deliverables, review the FILE CONTENTS (not just the text output). The text output may be a summary; the real work is in the files.
|
|
1471
|
+
|
|
1472
|
+
**Output format (JSON only, no other text):**
|
|
1473
|
+
{
|
|
1474
|
+
"approved": true/false,
|
|
1475
|
+
"feedback": "Detailed rejection feedback with specific issues (only if rejected)",
|
|
1476
|
+
"comment": "Brief approval comment (only if approved)"
|
|
1477
|
+
}`
|
|
1478
|
+
},
|
|
1479
|
+
{
|
|
1480
|
+
role: 'user',
|
|
1481
|
+
content: `Please review ${assignee.name}'s (${assignee.role}) work on task "${node.title}":
|
|
1482
|
+
|
|
1483
|
+
**Task description:** ${node.description}
|
|
1484
|
+
|
|
1485
|
+
**${round > 1 ? `Revised output (round ${round}):` : 'Agent output:'}**
|
|
1486
|
+
${outputContent || '(empty output)'}${fileContentsSection}
|
|
1487
|
+
|
|
1488
|
+
Please provide your review verdict as JSON.`
|
|
1489
|
+
},
|
|
1490
|
+
], { temperature: 0.3, maxTokens: 1024 });
|
|
1491
|
+
|
|
1492
|
+
// Parse review result
|
|
1493
|
+
const tick = String.fromCharCode(96);
|
|
1494
|
+
const fence = tick + tick + tick;
|
|
1495
|
+
let content = response.content?.trim() || '';
|
|
1496
|
+
content = content.replace(fence + 'json', '').replace(fence, '').trim();
|
|
1497
|
+
|
|
1498
|
+
try {
|
|
1499
|
+
const result = JSON.parse(content);
|
|
1500
|
+
return {
|
|
1501
|
+
approved: !!result.approved,
|
|
1502
|
+
feedback: result.feedback || '',
|
|
1503
|
+
comment: result.comment || '',
|
|
1504
|
+
};
|
|
1505
|
+
} catch (parseErr) {
|
|
1506
|
+
// If JSON parse fails, try to detect approval from text
|
|
1507
|
+
const lower = content.toLowerCase();
|
|
1508
|
+
if (lower.includes('"approved": true') || (lower.includes('approved') && !lower.includes('reject'))) {
|
|
1509
|
+
return { approved: true, feedback: '', comment: content.slice(0, 200) };
|
|
1510
|
+
}
|
|
1511
|
+
return { approved: false, feedback: content.slice(0, 500), comment: '' };
|
|
1512
|
+
}
|
|
1513
|
+
} catch (e) {
|
|
1514
|
+
console.error(`[StrictReview] ${reviewer.name} review failed:`, e.message);
|
|
1515
|
+
// Don't silently auto-approve on error — flag it so it's visible
|
|
1516
|
+
requirement?.addGroupMessage?.(
|
|
1517
|
+
{ name: 'System', role: 'system' },
|
|
1518
|
+
`⚠️ Review by ${reviewer.name} encountered an error: ${e.message}. Proceeding with caution (auto-approved due to error).`,
|
|
1519
|
+
'system', null, { auto: true }
|
|
1520
|
+
);
|
|
1521
|
+
return { approved: true, feedback: '', comment: `⚠️ Review error (auto-approved): ${e.message}` };
|
|
1522
|
+
}
|
|
1523
|
+
}
|
|
1524
|
+
|
|
1525
|
+
/**
|
|
1526
|
+
* Execute revision: agent revises their work based on review feedback
|
|
1527
|
+
* Similar to executeTask but with revision context
|
|
1528
|
+
*/
|
|
1529
|
+
async _executeRevision(agent, node, previousOutput, reviewFeedback, requirement, callbacks = {}) {
|
|
1530
|
+
const revisionTask = {
|
|
1531
|
+
title: `[Revision] ${node.title}`,
|
|
1532
|
+
description: `Your previous work on "${node.title}" was reviewed and REJECTED. You need to revise it.
|
|
1533
|
+
|
|
1534
|
+
**Original task description:**
|
|
1535
|
+
${node.description}
|
|
1536
|
+
|
|
1537
|
+
**Your previous output:**
|
|
1538
|
+
${previousOutput?.length > 1500 ? previousOutput.slice(0, 1500) + '\n...(truncated — use file_read to review your previous files before revising)' : previousOutput || '(empty)'}
|
|
1539
|
+
|
|
1540
|
+
**Reviewer's feedback (MUST address ALL points):**
|
|
1541
|
+
${reviewFeedback}
|
|
1542
|
+
|
|
1543
|
+
**Instructions:**
|
|
1544
|
+
1. Carefully read the reviewer's feedback
|
|
1545
|
+
2. Address EVERY issue mentioned by the reviewer
|
|
1546
|
+
3. If you wrote files before, READ them first, then MODIFY them (don't recreate from scratch)
|
|
1547
|
+
4. Make sure the revised output fully addresses all review comments
|
|
1548
|
+
5. Output your complete revised result`,
|
|
1549
|
+
context: '',
|
|
1550
|
+
requirements: `This is a REVISION for requirement "${requirement.title}". The reviewer was not satisfied and you must address their feedback.`,
|
|
1551
|
+
};
|
|
1552
|
+
|
|
1553
|
+
return await agent.executeTask(revisionTask, callbacks);
|
|
1554
|
+
}
|
|
1555
|
+
|
|
1556
|
+
/**
|
|
1557
|
+
* Assignee rebuttal: after receiving review rejection, the assignee can choose to
|
|
1558
|
+
* accept and revise, or push back (rebut) the reviewer's feedback.
|
|
1559
|
+
*
|
|
1560
|
+
* Returns { accept: boolean, message: string }
|
|
1561
|
+
* - accept=true: assignee agrees with feedback, will revise
|
|
1562
|
+
* - accept=false: assignee disagrees and provides counter-arguments
|
|
1563
|
+
*/
|
|
1564
|
+
async _assigneeRebuttal(agent, reviewer, node, currentOutput, reviewFeedback, requirement, round) {
|
|
1565
|
+
if (!agent.canChat()) {
|
|
1566
|
+
return { accept: true, message: 'Got it, I\'ll revise.' };
|
|
1567
|
+
}
|
|
1568
|
+
|
|
1569
|
+
try {
|
|
1570
|
+
const p = agent.personality || {};
|
|
1571
|
+
const outputPreview = currentOutput?.length > 1500
|
|
1572
|
+
? currentOutput.slice(0, 1500) + '\n...(truncated — use file_read to review the full content before deciding)'
|
|
1573
|
+
: currentOutput;
|
|
1574
|
+
|
|
1575
|
+
const response = await agent.chat([
|
|
1576
|
+
{
|
|
1577
|
+
role: 'system',
|
|
1578
|
+
content: `You are "${agent.name}", working as "${agent.role}".
|
|
1579
|
+
Your personality: ${p.trait || 'Professional'}. Speaking style: ${p.tone || 'Normal'}.
|
|
1580
|
+
|
|
1581
|
+
Your work on task "${node.title}" for requirement "${requirement.title}" was just reviewed and REJECTED by ${reviewer.name} (${reviewer.role}).
|
|
1582
|
+
|
|
1583
|
+
You now have a choice — this is a professional discussion, not a hierarchy:
|
|
1584
|
+
1. **Accept** the feedback and revise your work (if you genuinely agree the feedback is valid)
|
|
1585
|
+
2. **Push back** and argue your case (if you believe your work is correct or the feedback is unfair/wrong)
|
|
1586
|
+
|
|
1587
|
+
**Important guidelines for your decision:**
|
|
1588
|
+
- Be honest and professional. Don't just accept to be polite — if you genuinely believe your work is good, defend it!
|
|
1589
|
+
- Consider whether the reviewer's points are technically valid
|
|
1590
|
+
- If the feedback is about style preferences vs. actual errors, you can push back
|
|
1591
|
+
- If you made a genuine mistake, accept gracefully
|
|
1592
|
+
- When pushing back, provide SPECIFIC technical arguments, not just "I disagree"
|
|
1593
|
+
- Be respectful but firm — this is a professional debate between colleagues
|
|
1594
|
+
|
|
1595
|
+
**Output format (JSON only):**
|
|
1596
|
+
{
|
|
1597
|
+
"accept": true/false,
|
|
1598
|
+
"reasoning": "Your internal reasoning about the feedback (not shown to reviewer)",
|
|
1599
|
+
"message": "What you want to say to the reviewer (will be posted in group chat)"
|
|
1600
|
+
}`
|
|
1601
|
+
},
|
|
1602
|
+
{
|
|
1603
|
+
role: 'user',
|
|
1604
|
+
content: `Your work output:
|
|
1605
|
+
${outputPreview}
|
|
1606
|
+
|
|
1607
|
+
Reviewer ${reviewer.name}'s rejection feedback:
|
|
1608
|
+
${reviewFeedback}
|
|
1609
|
+
|
|
1610
|
+
This is review round ${round}. Do you accept the feedback and revise, or do you want to push back?`
|
|
1611
|
+
},
|
|
1612
|
+
], { temperature: 0.7, maxTokens: 512 });
|
|
1613
|
+
|
|
1614
|
+
const tick = String.fromCharCode(96);
|
|
1615
|
+
const fence = tick + tick + tick;
|
|
1616
|
+
let content = response.content?.trim() || '';
|
|
1617
|
+
content = content.replace(fence + 'json', '').replace(fence, '').trim();
|
|
1618
|
+
|
|
1619
|
+
try {
|
|
1620
|
+
const start = content.indexOf('{');
|
|
1621
|
+
const end = content.lastIndexOf('}');
|
|
1622
|
+
if (start !== -1 && end > start) {
|
|
1623
|
+
const result = JSON.parse(content.slice(start, end + 1));
|
|
1624
|
+
return {
|
|
1625
|
+
accept: !!result.accept,
|
|
1626
|
+
message: result.message || (result.accept ? 'Got it, I\'ll revise.' : 'I respectfully disagree with some of the feedback.'),
|
|
1627
|
+
};
|
|
1628
|
+
}
|
|
1629
|
+
} catch {}
|
|
1630
|
+
|
|
1631
|
+
// Parse failure fallback: accept
|
|
1632
|
+
return { accept: true, message: 'Got it, I\'ll revise based on your feedback.' };
|
|
1633
|
+
} catch (e) {
|
|
1634
|
+
console.error(`[AssigneeRebuttal] ${agent.name} rebuttal failed:`, e.message);
|
|
1635
|
+
return { accept: true, message: 'Got it, I\'ll revise.' };
|
|
1636
|
+
}
|
|
1637
|
+
}
|
|
1638
|
+
|
|
1639
|
+
/**
|
|
1640
|
+
* Reviewer re-evaluate: after the assignee pushes back with counter-arguments,
|
|
1641
|
+
* the reviewer decides whether they're convinced or if they stand firm.
|
|
1642
|
+
*
|
|
1643
|
+
* Returns { convinced: boolean, message: string }
|
|
1644
|
+
* - convinced=true: reviewer accepts the rebuttal, work is approved
|
|
1645
|
+
* - convinced=false: reviewer insists, assignee must revise
|
|
1646
|
+
*/
|
|
1647
|
+
async _reviewerReEvaluate(reviewer, assignee, node, output, originalFeedback, rebuttalMessage, requirement, round) {
|
|
1648
|
+
if (!reviewer.canChat()) {
|
|
1649
|
+
return { convinced: false, message: 'Please address my feedback.' };
|
|
1650
|
+
}
|
|
1651
|
+
|
|
1652
|
+
try {
|
|
1653
|
+
const p = reviewer.personality || {};
|
|
1654
|
+
const outputPreview = output?.length > 1000
|
|
1655
|
+
? output.slice(0, 1000) + '\n...(truncated — use file_read to review the full content before deciding)'
|
|
1656
|
+
: output;
|
|
1657
|
+
|
|
1658
|
+
const response = await reviewer.chat([
|
|
1659
|
+
{
|
|
1660
|
+
role: 'system',
|
|
1661
|
+
content: `You are "${reviewer.name}", working as "${reviewer.role}".
|
|
1662
|
+
Your personality: ${p.trait || 'Professional'}. Speaking style: ${p.tone || 'Normal'}.
|
|
1663
|
+
|
|
1664
|
+
You rejected ${assignee.name}'s work on "${node.title}", but they pushed back with counter-arguments.
|
|
1665
|
+
Now you need to decide: were you wrong, or do you stand firm?
|
|
1666
|
+
|
|
1667
|
+
**Guidelines:**
|
|
1668
|
+
- Be open-minded — good reviewers can admit when they're wrong
|
|
1669
|
+
- If the assignee makes valid technical points that address your concerns, be convinced
|
|
1670
|
+
- If the assignee is just making excuses without substance, stand firm
|
|
1671
|
+
- If it's a matter of style/preference rather than correctness, consider being flexible
|
|
1672
|
+
- Don't be stubborn for the sake of it — the goal is quality, not winning arguments
|
|
1673
|
+
- Be approximately 30-40% likely to be convinced if the argument has some merit
|
|
1674
|
+
|
|
1675
|
+
**Output format (JSON only):**
|
|
1676
|
+
{
|
|
1677
|
+
"convinced": true/false,
|
|
1678
|
+
"reasoning": "Your internal reasoning (not shown to assignee)",
|
|
1679
|
+
"message": "What you want to say in response (will be posted in group chat)"
|
|
1680
|
+
}`
|
|
1681
|
+
},
|
|
1682
|
+
{
|
|
1683
|
+
role: 'user',
|
|
1684
|
+
content: `Your original rejection feedback:
|
|
1685
|
+
${originalFeedback}
|
|
1686
|
+
|
|
1687
|
+
${assignee.name}'s counter-argument:
|
|
1688
|
+
${rebuttalMessage}
|
|
1689
|
+
|
|
1690
|
+
The work in question:
|
|
1691
|
+
${outputPreview}
|
|
1692
|
+
|
|
1693
|
+
Are you convinced by their argument, or do you insist they need to revise?`
|
|
1694
|
+
},
|
|
1695
|
+
], { temperature: 0.6, maxTokens: 512 });
|
|
1696
|
+
|
|
1697
|
+
const tick = String.fromCharCode(96);
|
|
1698
|
+
const fence = tick + tick + tick;
|
|
1699
|
+
let content = response.content?.trim() || '';
|
|
1700
|
+
content = content.replace(fence + 'json', '').replace(fence, '').trim();
|
|
1701
|
+
|
|
1702
|
+
try {
|
|
1703
|
+
const start = content.indexOf('{');
|
|
1704
|
+
const end = content.lastIndexOf('}');
|
|
1705
|
+
if (start !== -1 && end > start) {
|
|
1706
|
+
const result = JSON.parse(content.slice(start, end + 1));
|
|
1707
|
+
return {
|
|
1708
|
+
convinced: !!result.convinced,
|
|
1709
|
+
message: result.message || (result.convinced ? 'You make a fair point, approved!' : 'I still think the issues need to be addressed.'),
|
|
1710
|
+
};
|
|
1711
|
+
}
|
|
1712
|
+
} catch {}
|
|
1713
|
+
|
|
1714
|
+
return { convinced: false, message: 'I appreciate your perspective, but please address my feedback.' };
|
|
1715
|
+
} catch (e) {
|
|
1716
|
+
console.error(`[ReviewerReEvaluate] ${reviewer.name} re-evaluation failed:`, e.message);
|
|
1717
|
+
return { convinced: false, message: 'Please address my feedback.' };
|
|
1718
|
+
}
|
|
1719
|
+
}
|
|
1720
|
+
|
|
1721
|
+
/**
|
|
1722
|
+
* Record agent-to-agent chat message in chatStore
|
|
1723
|
+
* Session format: agent-agent-{smallerId}-{largerId} (consistent ordering)
|
|
1724
|
+
*/
|
|
1725
|
+
_recordAgentChat(fromAgent, toAgent, content) {
|
|
1726
|
+
const ids = [fromAgent.id, toAgent.id].sort();
|
|
1727
|
+
const sessionId = `agent-agent-${ids[0]}-${ids[1]}`;
|
|
1728
|
+
chatStore.createSession(sessionId, {
|
|
1729
|
+
title: `${fromAgent.name} & ${toAgent.name}`,
|
|
1730
|
+
participants: [fromAgent.id, toAgent.id],
|
|
1731
|
+
participantNames: [fromAgent.name, toAgent.name],
|
|
1732
|
+
type: 'agent-agent',
|
|
1733
|
+
});
|
|
1734
|
+
chatStore.appendMessage(sessionId, {
|
|
1735
|
+
role: 'agent',
|
|
1736
|
+
content,
|
|
1737
|
+
time: new Date(),
|
|
1738
|
+
fromAgentId: fromAgent.id,
|
|
1739
|
+
fromAgentName: fromAgent.name,
|
|
1740
|
+
toAgentId: toAgent.id,
|
|
1741
|
+
toAgentName: toAgent.name,
|
|
1742
|
+
});
|
|
1743
|
+
}
|
|
1744
|
+
|
|
1745
|
+
/**
|
|
1746
|
+
* Clean LLM output by stripping tool-call markup that some models embed in text
|
|
1747
|
+
* (e.g. DeepSeek's DSML format, or XML-style function_call tags)
|
|
1748
|
+
*/
|
|
1749
|
+
_cleanLLMOutput(text) {
|
|
1750
|
+
if (!text) return '';
|
|
1751
|
+
let cleaned = text;
|
|
1752
|
+
// Strip DSML-style tool call blocks: <|DSML|function_calls>...</|DSML|function_calls> or trailing
|
|
1753
|
+
cleaned = cleaned.replace(/<|DSML|function_calls>[\s\S]*?<\/|DSML|function_calls>/g, '');
|
|
1754
|
+
// Strip trailing incomplete DSML block (when output was truncated mid-tool-call)
|
|
1755
|
+
cleaned = cleaned.replace(/<|DSML|[\s\S]*$/g, '');
|
|
1756
|
+
// Strip generic XML-style tool call blocks: <function_call>...</function_call>
|
|
1757
|
+
cleaned = cleaned.replace(/<function_call>[\s\S]*?<\/function_call>/g, '');
|
|
1758
|
+
cleaned = cleaned.replace(/<function_call>[\s\S]*$/g, '');
|
|
1759
|
+
// Strip <tool_call>...</tool_call> blocks
|
|
1760
|
+
cleaned = cleaned.replace(/<tool_call>[\s\S]*?<\/tool_call>/g, '');
|
|
1761
|
+
cleaned = cleaned.replace(/<tool_call>[\s\S]*$/g, '');
|
|
1762
|
+
return cleaned.trim();
|
|
1763
|
+
}
|
|
1764
|
+
|
|
1765
|
+
/**
|
|
1766
|
+
* Detect output type
|
|
1767
|
+
*/
|
|
1768
|
+
_detectOutputType(result) {
|
|
1769
|
+
const toolNames = (result.toolResults || []).map(t => t.tool);
|
|
1770
|
+
if (toolNames.includes('file_write')) return 'code';
|
|
1771
|
+
if (result.output?.includes('```')) return 'code';
|
|
1772
|
+
return 'text';
|
|
1773
|
+
}
|
|
1774
|
+
|
|
1775
|
+
/** Serialize all requirements */
|
|
1776
|
+
serialize() {
|
|
1777
|
+
return [...this.requirements.values()].map(r => r.serialize());
|
|
1778
|
+
}
|
|
1779
|
+
|
|
1780
|
+
/** Deserialize */
|
|
1781
|
+
static deserialize(dataList) {
|
|
1782
|
+
const mgr = new RequirementManager();
|
|
1783
|
+
for (const d of (dataList || [])) {
|
|
1784
|
+
const req = Requirement.deserialize(d);
|
|
1785
|
+
mgr.requirements.set(req.id, req);
|
|
1786
|
+
}
|
|
1787
|
+
return mgr;
|
|
1788
|
+
}
|
|
1789
|
+
}
|