claudehq 1.0.2 → 1.0.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/lib/core/claude-events.js +2 -1
- package/lib/core/config.js +39 -1
- package/lib/data/orchestration.js +941 -0
- package/lib/index.js +211 -23
- package/lib/orchestration/executor.js +635 -0
- package/lib/routes/orchestration.js +417 -0
- package/lib/routes/spawner.js +335 -0
- package/lib/sessions/manager.js +36 -9
- package/lib/spawner/index.js +51 -0
- package/lib/spawner/path-validator.js +366 -0
- package/lib/spawner/projects-manager.js +421 -0
- package/lib/spawner/session-spawner.js +1010 -0
- package/package.json +1 -1
- package/public/index.html +399 -18
- package/lib/server.js +0 -9364
|
@@ -0,0 +1,635 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Orchestration Executor - Manages agent spawning and execution
|
|
3
|
+
*
|
|
4
|
+
* Handles spawning Claude agents via tmux, managing their lifecycle,
|
|
5
|
+
* and coordinating execution based on dependency graphs.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const { execFile } = require('child_process');
|
|
9
|
+
const crypto = require('crypto');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
const fs = require('fs');
|
|
12
|
+
|
|
13
|
+
const { AGENT_STATUS, ORCHESTRATION_STATUS } = require('../core/config');
|
|
14
|
+
const { eventBus, EventTypes } = require('../core/event-bus');
|
|
15
|
+
const { broadcastUpdate } = require('../core/sse');
|
|
16
|
+
const orchestrationData = require('../data/orchestration');
|
|
17
|
+
|
|
18
|
+
// Track active agent sessions
|
|
19
|
+
const activeAgents = new Map(); // agentId -> { tmuxSession, orchestrationId }
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Generate a unique tmux session name for an agent
|
|
23
|
+
* @param {string} agentId - Agent ID
|
|
24
|
+
* @returns {string} Tmux session name
|
|
25
|
+
*/
|
|
26
|
+
function generateTmuxSessionName(agentId) {
|
|
27
|
+
const shortId = crypto.randomBytes(3).toString('hex');
|
|
28
|
+
return `orch-${agentId.slice(-8)}-${shortId}`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Build the claude command with appropriate flags
|
|
33
|
+
* @param {Object} agent - Agent configuration
|
|
34
|
+
* @returns {string} Claude command string
|
|
35
|
+
*/
|
|
36
|
+
function buildClaudeCommand(agent) {
|
|
37
|
+
const args = [];
|
|
38
|
+
|
|
39
|
+
// Model selection
|
|
40
|
+
if (agent.model && agent.model !== 'sonnet') {
|
|
41
|
+
args.push(`--model ${agent.model}`);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Always continue conversation if available
|
|
45
|
+
args.push('-c');
|
|
46
|
+
|
|
47
|
+
// Skip permissions for orchestrated agents (they're managed)
|
|
48
|
+
args.push('--dangerously-skip-permissions');
|
|
49
|
+
|
|
50
|
+
// If there's an initial prompt, use -p flag for headless mode initially
|
|
51
|
+
// then switch to interactive
|
|
52
|
+
if (agent.prompt) {
|
|
53
|
+
// We'll send the prompt after spawning
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return `claude ${args.join(' ')}`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Spawn an agent in a new tmux window
|
|
61
|
+
* @param {string} orchestrationId - Orchestration ID
|
|
62
|
+
* @param {string} agentId - Agent ID
|
|
63
|
+
* @returns {Promise<Object>} Result with session info or error
|
|
64
|
+
*/
|
|
65
|
+
async function spawnAgent(orchestrationId, agentId) {
|
|
66
|
+
const agent = orchestrationData.getAgent(orchestrationId, agentId);
|
|
67
|
+
|
|
68
|
+
if (!agent) {
|
|
69
|
+
return { error: 'Agent not found' };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (agent.status !== AGENT_STATUS.PENDING) {
|
|
73
|
+
return { error: `Agent is not in pending state (current: ${agent.status})` };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Check dependencies
|
|
77
|
+
const orchestration = orchestrationData.getOrchestration(orchestrationId);
|
|
78
|
+
for (const depId of agent.dependencies) {
|
|
79
|
+
const depAgent = orchestration.agents.find(a => a.id === depId);
|
|
80
|
+
if (!depAgent || depAgent.status !== AGENT_STATUS.COMPLETED) {
|
|
81
|
+
return { error: `Dependency ${depId} is not completed` };
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Update status to spawning
|
|
86
|
+
orchestrationData.updateAgent(orchestrationId, agentId, {
|
|
87
|
+
status: AGENT_STATUS.SPAWNING
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
const tmuxSessionName = generateTmuxSessionName(agentId);
|
|
91
|
+
const cwd = agent.workingDirectory || process.cwd();
|
|
92
|
+
|
|
93
|
+
// Ensure working directory exists
|
|
94
|
+
if (!fs.existsSync(cwd)) {
|
|
95
|
+
orchestrationData.updateAgent(orchestrationId, agentId, {
|
|
96
|
+
status: AGENT_STATUS.FAILED,
|
|
97
|
+
error: `Working directory does not exist: ${cwd}`
|
|
98
|
+
});
|
|
99
|
+
return { error: `Working directory does not exist: ${cwd}` };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const claudeCmd = buildClaudeCommand(agent);
|
|
103
|
+
|
|
104
|
+
return new Promise((resolve) => {
|
|
105
|
+
execFile('tmux', [
|
|
106
|
+
'new-session', '-d', '-s', tmuxSessionName, '-c', cwd, claudeCmd
|
|
107
|
+
], (error) => {
|
|
108
|
+
if (error) {
|
|
109
|
+
console.error(`Failed to spawn agent ${agentId}: ${error.message}`);
|
|
110
|
+
orchestrationData.updateAgent(orchestrationId, agentId, {
|
|
111
|
+
status: AGENT_STATUS.FAILED,
|
|
112
|
+
error: `Failed to spawn: ${error.message}`
|
|
113
|
+
});
|
|
114
|
+
resolve({ error: error.message });
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Track the active agent
|
|
119
|
+
activeAgents.set(agentId, {
|
|
120
|
+
tmuxSession: tmuxSessionName,
|
|
121
|
+
orchestrationId
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
// Update agent with tmux session info
|
|
125
|
+
orchestrationData.updateAgent(orchestrationId, agentId, {
|
|
126
|
+
status: AGENT_STATUS.RUNNING,
|
|
127
|
+
tmuxWindow: tmuxSessionName,
|
|
128
|
+
startedAt: new Date().toISOString()
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
console.log(` Spawned agent "${agent.name}" (${agentId.slice(-8)}) -> tmux:${tmuxSessionName}`);
|
|
132
|
+
|
|
133
|
+
// If there's an initial prompt, send it after a short delay
|
|
134
|
+
if (agent.prompt) {
|
|
135
|
+
setTimeout(() => {
|
|
136
|
+
sendPromptToAgent(orchestrationId, agentId, agent.prompt);
|
|
137
|
+
}, 1000);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
resolve({
|
|
141
|
+
success: true,
|
|
142
|
+
tmuxSession: tmuxSessionName,
|
|
143
|
+
agentId
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Send a prompt to an agent
|
|
151
|
+
* @param {string} orchestrationId - Orchestration ID
|
|
152
|
+
* @param {string} agentId - Agent ID
|
|
153
|
+
* @param {string} prompt - Prompt text
|
|
154
|
+
* @returns {Promise<Object>} Result
|
|
155
|
+
*/
|
|
156
|
+
async function sendPromptToAgent(orchestrationId, agentId, prompt) {
|
|
157
|
+
const agentInfo = activeAgents.get(agentId);
|
|
158
|
+
|
|
159
|
+
if (!agentInfo) {
|
|
160
|
+
return { error: 'Agent not found or not running' };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return new Promise((resolve, reject) => {
|
|
164
|
+
const tempFile = `/tmp/orch-prompt-${Date.now()}.txt`;
|
|
165
|
+
fs.writeFileSync(tempFile, prompt);
|
|
166
|
+
|
|
167
|
+
execFile('tmux', ['load-buffer', tempFile], (err) => {
|
|
168
|
+
if (err) {
|
|
169
|
+
fs.unlinkSync(tempFile);
|
|
170
|
+
return resolve({ error: `tmux load-buffer failed: ${err.message}` });
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
execFile('tmux', ['paste-buffer', '-t', agentInfo.tmuxSession], (err2) => {
|
|
174
|
+
fs.unlinkSync(tempFile);
|
|
175
|
+
|
|
176
|
+
if (err2) {
|
|
177
|
+
return resolve({ error: `tmux paste-buffer failed: ${err2.message}` });
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
setTimeout(() => {
|
|
181
|
+
execFile('tmux', ['send-keys', '-t', agentInfo.tmuxSession, 'Enter'], (err3) => {
|
|
182
|
+
if (err3) {
|
|
183
|
+
return resolve({ error: `tmux send-keys failed: ${err3.message}` });
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
eventBus.emit(EventTypes.AGENT_OUTPUT, {
|
|
187
|
+
orchestrationId,
|
|
188
|
+
agentId,
|
|
189
|
+
type: 'prompt_sent',
|
|
190
|
+
prompt
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
resolve({ success: true });
|
|
194
|
+
});
|
|
195
|
+
}, 50);
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Kill an agent
|
|
203
|
+
* @param {string} orchestrationId - Orchestration ID
|
|
204
|
+
* @param {string} agentId - Agent ID
|
|
205
|
+
* @returns {Promise<Object>} Result
|
|
206
|
+
*/
|
|
207
|
+
async function killAgent(orchestrationId, agentId) {
|
|
208
|
+
const agentInfo = activeAgents.get(agentId);
|
|
209
|
+
|
|
210
|
+
if (!agentInfo) {
|
|
211
|
+
// Agent may not be spawned yet, just update status
|
|
212
|
+
orchestrationData.updateAgent(orchestrationId, agentId, {
|
|
213
|
+
status: AGENT_STATUS.CANCELLED,
|
|
214
|
+
completedAt: new Date().toISOString()
|
|
215
|
+
});
|
|
216
|
+
return { success: true };
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
return new Promise((resolve) => {
|
|
220
|
+
execFile('tmux', ['kill-session', '-t', agentInfo.tmuxSession], (error) => {
|
|
221
|
+
if (error) {
|
|
222
|
+
console.log(` Note: tmux session ${agentInfo.tmuxSession} may already be dead`);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
activeAgents.delete(agentId);
|
|
226
|
+
|
|
227
|
+
orchestrationData.updateAgent(orchestrationId, agentId, {
|
|
228
|
+
status: AGENT_STATUS.CANCELLED,
|
|
229
|
+
completedAt: new Date().toISOString()
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
console.log(` Killed agent ${agentId.slice(-8)}`);
|
|
233
|
+
resolve({ success: true });
|
|
234
|
+
});
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Mark an agent as completed
|
|
240
|
+
* @param {string} orchestrationId - Orchestration ID
|
|
241
|
+
* @param {string} agentId - Agent ID
|
|
242
|
+
* @param {Object} result - Optional result data
|
|
243
|
+
* @returns {Object} Result
|
|
244
|
+
*/
|
|
245
|
+
function completeAgent(orchestrationId, agentId, result = {}) {
|
|
246
|
+
activeAgents.delete(agentId);
|
|
247
|
+
|
|
248
|
+
orchestrationData.updateAgent(orchestrationId, agentId, {
|
|
249
|
+
status: AGENT_STATUS.COMPLETED,
|
|
250
|
+
completedAt: new Date().toISOString(),
|
|
251
|
+
output: result.output || null
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
// Check if any dependent agents can now run
|
|
255
|
+
checkAndSpawnReadyAgents(orchestrationId);
|
|
256
|
+
|
|
257
|
+
// Check if orchestration is complete
|
|
258
|
+
if (orchestrationData.isOrchestrationComplete(orchestrationId)) {
|
|
259
|
+
orchestrationData.updateOrchestration(orchestrationId, {
|
|
260
|
+
status: ORCHESTRATION_STATUS.COMPLETED,
|
|
261
|
+
completedAt: new Date().toISOString()
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
return { success: true };
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Mark an agent as failed
|
|
270
|
+
* @param {string} orchestrationId - Orchestration ID
|
|
271
|
+
* @param {string} agentId - Agent ID
|
|
272
|
+
* @param {string} error - Error message
|
|
273
|
+
* @returns {Object} Result
|
|
274
|
+
*/
|
|
275
|
+
function failAgent(orchestrationId, agentId, error) {
|
|
276
|
+
activeAgents.delete(agentId);
|
|
277
|
+
|
|
278
|
+
orchestrationData.updateAgent(orchestrationId, agentId, {
|
|
279
|
+
status: AGENT_STATUS.FAILED,
|
|
280
|
+
completedAt: new Date().toISOString(),
|
|
281
|
+
error
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
// Check if orchestration should fail
|
|
285
|
+
const orchestration = orchestrationData.getOrchestration(orchestrationId);
|
|
286
|
+
const failOnError = orchestration.metadata?.failOnError !== false;
|
|
287
|
+
|
|
288
|
+
if (failOnError) {
|
|
289
|
+
// Mark orchestration as failed
|
|
290
|
+
orchestrationData.updateOrchestration(orchestrationId, {
|
|
291
|
+
status: ORCHESTRATION_STATUS.FAILED,
|
|
292
|
+
completedAt: new Date().toISOString()
|
|
293
|
+
});
|
|
294
|
+
} else {
|
|
295
|
+
// Continue with other agents
|
|
296
|
+
checkAndSpawnReadyAgents(orchestrationId);
|
|
297
|
+
|
|
298
|
+
if (orchestrationData.isOrchestrationComplete(orchestrationId)) {
|
|
299
|
+
orchestrationData.updateOrchestration(orchestrationId, {
|
|
300
|
+
status: ORCHESTRATION_STATUS.COMPLETED,
|
|
301
|
+
completedAt: new Date().toISOString()
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
return { success: true };
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Check for agents ready to run and spawn them
|
|
311
|
+
* @param {string} orchestrationId - Orchestration ID
|
|
312
|
+
*/
|
|
313
|
+
async function checkAndSpawnReadyAgents(orchestrationId) {
|
|
314
|
+
const orchestration = orchestrationData.getOrchestration(orchestrationId);
|
|
315
|
+
|
|
316
|
+
if (!orchestration || orchestration.status !== ORCHESTRATION_STATUS.RUNNING) {
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const readyAgents = orchestrationData.getReadyAgents(orchestrationId);
|
|
321
|
+
|
|
322
|
+
for (const agent of readyAgents) {
|
|
323
|
+
console.log(` Auto-spawning ready agent: ${agent.name}`);
|
|
324
|
+
await spawnAgent(orchestrationId, agent.id);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Start an orchestration (spawn all ready agents)
|
|
330
|
+
* @param {string} orchestrationId - Orchestration ID
|
|
331
|
+
* @returns {Promise<Object>} Result
|
|
332
|
+
*/
|
|
333
|
+
async function startOrchestration(orchestrationId) {
|
|
334
|
+
const orchestration = orchestrationData.getOrchestration(orchestrationId);
|
|
335
|
+
|
|
336
|
+
if (!orchestration) {
|
|
337
|
+
return { error: 'Orchestration not found' };
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
if (orchestration.status === ORCHESTRATION_STATUS.RUNNING) {
|
|
341
|
+
return { error: 'Orchestration is already running' };
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
if (orchestration.status === ORCHESTRATION_STATUS.COMPLETED) {
|
|
345
|
+
return { error: 'Orchestration is already completed' };
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
if (orchestration.agents.length === 0) {
|
|
349
|
+
return { error: 'Orchestration has no agents' };
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// Update orchestration status
|
|
353
|
+
orchestrationData.updateOrchestration(orchestrationId, {
|
|
354
|
+
status: ORCHESTRATION_STATUS.RUNNING,
|
|
355
|
+
startedAt: new Date().toISOString()
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
// Spawn all ready agents (those with no dependencies or all deps completed)
|
|
359
|
+
const readyAgents = orchestrationData.getReadyAgents(orchestrationId);
|
|
360
|
+
|
|
361
|
+
if (readyAgents.length === 0) {
|
|
362
|
+
return { error: 'No agents are ready to run (check dependencies)' };
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
console.log(` Starting orchestration "${orchestration.name}" with ${readyAgents.length} ready agents`);
|
|
366
|
+
|
|
367
|
+
const results = [];
|
|
368
|
+
for (const agent of readyAgents) {
|
|
369
|
+
const result = await spawnAgent(orchestrationId, agent.id);
|
|
370
|
+
results.push({ agentId: agent.id, ...result });
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
return {
|
|
374
|
+
success: true,
|
|
375
|
+
orchestrationId,
|
|
376
|
+
spawnedAgents: results.filter(r => r.success).length,
|
|
377
|
+
results
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Stop an orchestration (kill all running agents)
|
|
383
|
+
* @param {string} orchestrationId - Orchestration ID
|
|
384
|
+
* @returns {Promise<Object>} Result
|
|
385
|
+
*/
|
|
386
|
+
async function stopOrchestration(orchestrationId) {
|
|
387
|
+
const orchestration = orchestrationData.getOrchestration(orchestrationId);
|
|
388
|
+
|
|
389
|
+
if (!orchestration) {
|
|
390
|
+
return { error: 'Orchestration not found' };
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
console.log(` Stopping orchestration "${orchestration.name}"`);
|
|
394
|
+
|
|
395
|
+
// Kill all running/spawning agents
|
|
396
|
+
const runningAgents = orchestration.agents.filter(a =>
|
|
397
|
+
a.status === AGENT_STATUS.RUNNING || a.status === AGENT_STATUS.SPAWNING
|
|
398
|
+
);
|
|
399
|
+
|
|
400
|
+
for (const agent of runningAgents) {
|
|
401
|
+
await killAgent(orchestrationId, agent.id);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// Mark pending agents as cancelled
|
|
405
|
+
const pendingAgents = orchestration.agents.filter(a => a.status === AGENT_STATUS.PENDING);
|
|
406
|
+
for (const agent of pendingAgents) {
|
|
407
|
+
orchestrationData.updateAgent(orchestrationId, agent.id, {
|
|
408
|
+
status: AGENT_STATUS.CANCELLED
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// Update orchestration status
|
|
413
|
+
orchestrationData.updateOrchestration(orchestrationId, {
|
|
414
|
+
status: ORCHESTRATION_STATUS.PAUSED
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
return {
|
|
418
|
+
success: true,
|
|
419
|
+
killedAgents: runningAgents.length,
|
|
420
|
+
cancelledAgents: pendingAgents.length
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* Check health of all active agents
|
|
426
|
+
*/
|
|
427
|
+
function checkAgentHealth() {
|
|
428
|
+
execFile('tmux', ['list-sessions', '-F', '#{session_name}'], (error, stdout) => {
|
|
429
|
+
if (error) {
|
|
430
|
+
// No tmux sessions - mark all agents as offline/failed
|
|
431
|
+
for (const [agentId, info] of activeAgents) {
|
|
432
|
+
console.log(` Agent ${agentId.slice(-8)} tmux session died`);
|
|
433
|
+
failAgent(info.orchestrationId, agentId, 'Tmux session died');
|
|
434
|
+
}
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
const activeSessions = new Set(stdout.trim().split('\n').filter(Boolean));
|
|
439
|
+
|
|
440
|
+
for (const [agentId, info] of activeAgents) {
|
|
441
|
+
if (!activeSessions.has(info.tmuxSession)) {
|
|
442
|
+
console.log(` Agent ${agentId.slice(-8)} tmux session no longer exists`);
|
|
443
|
+
// Session died - this could mean the agent completed or crashed
|
|
444
|
+
// For now, mark as completed (user can check output)
|
|
445
|
+
completeAgent(info.orchestrationId, agentId, {
|
|
446
|
+
output: 'Session ended'
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* Get status of an agent's tmux session
|
|
455
|
+
* @param {string} agentId - Agent ID
|
|
456
|
+
* @returns {Promise<Object>} Status info
|
|
457
|
+
*/
|
|
458
|
+
async function getAgentTmuxStatus(agentId) {
|
|
459
|
+
const agentInfo = activeAgents.get(agentId);
|
|
460
|
+
|
|
461
|
+
if (!agentInfo) {
|
|
462
|
+
return { active: false };
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
return new Promise((resolve) => {
|
|
466
|
+
execFile('tmux', ['capture-pane', '-t', agentInfo.tmuxSession, '-p', '-S', '-30'],
|
|
467
|
+
{ timeout: 2000, maxBuffer: 1024 * 1024 },
|
|
468
|
+
(error, stdout) => {
|
|
469
|
+
if (error) {
|
|
470
|
+
resolve({ active: false, error: error.message });
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
resolve({
|
|
475
|
+
active: true,
|
|
476
|
+
tmuxSession: agentInfo.tmuxSession,
|
|
477
|
+
recentOutput: stdout
|
|
478
|
+
});
|
|
479
|
+
}
|
|
480
|
+
);
|
|
481
|
+
});
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
/**
|
|
485
|
+
* Link a Claude session event to an orchestration agent
|
|
486
|
+
* @param {string} claudeSessionId - Claude session ID from hook event
|
|
487
|
+
* @param {string} orchestrationId - Orchestration ID
|
|
488
|
+
* @param {string} agentId - Agent ID
|
|
489
|
+
*/
|
|
490
|
+
function linkClaudeSessionToAgent(claudeSessionId, orchestrationId, agentId) {
|
|
491
|
+
orchestrationData.updateAgent(orchestrationId, agentId, {
|
|
492
|
+
sessionId: claudeSessionId
|
|
493
|
+
});
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
/**
|
|
497
|
+
* Find agent by Claude session ID
|
|
498
|
+
* @param {string} claudeSessionId - Claude session ID
|
|
499
|
+
* @returns {Object|null} Agent info or null
|
|
500
|
+
*/
|
|
501
|
+
function findAgentByClaudeSession(claudeSessionId) {
|
|
502
|
+
const orchestrations = orchestrationData.listOrchestrations();
|
|
503
|
+
|
|
504
|
+
for (const orch of orchestrations) {
|
|
505
|
+
for (const agent of orch.agents) {
|
|
506
|
+
if (agent.sessionId === claudeSessionId) {
|
|
507
|
+
return { orchestrationId: orch.id, agent };
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
return null;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
/**
|
|
516
|
+
* Start health check interval
|
|
517
|
+
*/
|
|
518
|
+
let healthCheckInterval = null;
|
|
519
|
+
|
|
520
|
+
function startHealthChecks() {
|
|
521
|
+
if (healthCheckInterval) {
|
|
522
|
+
clearInterval(healthCheckInterval);
|
|
523
|
+
}
|
|
524
|
+
healthCheckInterval = setInterval(checkAgentHealth, 5000);
|
|
525
|
+
console.log(' Orchestration health monitoring started (5s interval)');
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
function stopHealthChecks() {
|
|
529
|
+
if (healthCheckInterval) {
|
|
530
|
+
clearInterval(healthCheckInterval);
|
|
531
|
+
healthCheckInterval = null;
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
// Register event handlers for SSE broadcasts
|
|
536
|
+
function registerEventHandlers() {
|
|
537
|
+
// Orchestration events
|
|
538
|
+
eventBus.on(EventTypes.ORCHESTRATION_CREATED, (data) => {
|
|
539
|
+
broadcastUpdate('orchestration_created', data);
|
|
540
|
+
});
|
|
541
|
+
|
|
542
|
+
eventBus.on(EventTypes.ORCHESTRATION_UPDATED, (data) => {
|
|
543
|
+
broadcastUpdate('orchestration_updated', data);
|
|
544
|
+
});
|
|
545
|
+
|
|
546
|
+
eventBus.on(EventTypes.ORCHESTRATION_STARTED, (data) => {
|
|
547
|
+
broadcastUpdate('orchestration_started', data);
|
|
548
|
+
});
|
|
549
|
+
|
|
550
|
+
eventBus.on(EventTypes.ORCHESTRATION_COMPLETED, (data) => {
|
|
551
|
+
broadcastUpdate('orchestration_completed', data);
|
|
552
|
+
});
|
|
553
|
+
|
|
554
|
+
eventBus.on(EventTypes.ORCHESTRATION_FAILED, (data) => {
|
|
555
|
+
broadcastUpdate('orchestration_failed', data);
|
|
556
|
+
});
|
|
557
|
+
|
|
558
|
+
eventBus.on(EventTypes.ORCHESTRATION_PAUSED, (data) => {
|
|
559
|
+
broadcastUpdate('orchestration_paused', data);
|
|
560
|
+
});
|
|
561
|
+
|
|
562
|
+
eventBus.on(EventTypes.ORCHESTRATION_DELETED, (data) => {
|
|
563
|
+
broadcastUpdate('orchestration_deleted', data);
|
|
564
|
+
});
|
|
565
|
+
|
|
566
|
+
// Agent events
|
|
567
|
+
eventBus.on(EventTypes.AGENT_CREATED, (data) => {
|
|
568
|
+
broadcastUpdate('agent_created', data);
|
|
569
|
+
});
|
|
570
|
+
|
|
571
|
+
eventBus.on(EventTypes.AGENT_SPAWNED, (data) => {
|
|
572
|
+
broadcastUpdate('agent_spawned', data);
|
|
573
|
+
});
|
|
574
|
+
|
|
575
|
+
eventBus.on(EventTypes.AGENT_STATUS_CHANGED, (data) => {
|
|
576
|
+
broadcastUpdate('agent_status_changed', data);
|
|
577
|
+
});
|
|
578
|
+
|
|
579
|
+
eventBus.on(EventTypes.AGENT_OUTPUT, (data) => {
|
|
580
|
+
broadcastUpdate('agent_output', data);
|
|
581
|
+
});
|
|
582
|
+
|
|
583
|
+
eventBus.on(EventTypes.AGENT_COMPLETED, (data) => {
|
|
584
|
+
broadcastUpdate('agent_completed', data);
|
|
585
|
+
});
|
|
586
|
+
|
|
587
|
+
eventBus.on(EventTypes.AGENT_FAILED, (data) => {
|
|
588
|
+
broadcastUpdate('agent_failed', data);
|
|
589
|
+
});
|
|
590
|
+
|
|
591
|
+
eventBus.on(EventTypes.AGENT_KILLED, (data) => {
|
|
592
|
+
broadcastUpdate('agent_killed', data);
|
|
593
|
+
});
|
|
594
|
+
|
|
595
|
+
console.log(' Orchestration event handlers registered');
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
/**
|
|
599
|
+
* Initialize the executor
|
|
600
|
+
*/
|
|
601
|
+
function init() {
|
|
602
|
+
registerEventHandlers();
|
|
603
|
+
startHealthChecks();
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
module.exports = {
|
|
607
|
+
// Initialization
|
|
608
|
+
init,
|
|
609
|
+
startHealthChecks,
|
|
610
|
+
stopHealthChecks,
|
|
611
|
+
registerEventHandlers,
|
|
612
|
+
|
|
613
|
+
// Agent lifecycle
|
|
614
|
+
spawnAgent,
|
|
615
|
+
killAgent,
|
|
616
|
+
completeAgent,
|
|
617
|
+
failAgent,
|
|
618
|
+
sendPromptToAgent,
|
|
619
|
+
|
|
620
|
+
// Orchestration lifecycle
|
|
621
|
+
startOrchestration,
|
|
622
|
+
stopOrchestration,
|
|
623
|
+
checkAndSpawnReadyAgents,
|
|
624
|
+
|
|
625
|
+
// Status
|
|
626
|
+
getAgentTmuxStatus,
|
|
627
|
+
checkAgentHealth,
|
|
628
|
+
|
|
629
|
+
// Session linking
|
|
630
|
+
linkClaudeSessionToAgent,
|
|
631
|
+
findAgentByClaudeSession,
|
|
632
|
+
|
|
633
|
+
// Direct access
|
|
634
|
+
activeAgents
|
|
635
|
+
};
|