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,941 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Orchestration Data Module - Multi-Agent Orchestration Management
|
|
3
|
+
*
|
|
4
|
+
* Handles creating, updating, and managing orchestrations and their agents.
|
|
5
|
+
* Orchestrations coordinate multiple Claude agents working in parallel.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
const path = require('path');
|
|
10
|
+
const crypto = require('crypto');
|
|
11
|
+
|
|
12
|
+
const { ORCHESTRATIONS_DIR, AGENT_STATUS, ORCHESTRATION_STATUS } = require('../core/config');
|
|
13
|
+
const { eventBus, EventTypes } = require('../core/event-bus');
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Built-in orchestration templates
|
|
17
|
+
*/
|
|
18
|
+
const ORCHESTRATION_TEMPLATES = {
|
|
19
|
+
'code-review': {
|
|
20
|
+
id: 'code-review',
|
|
21
|
+
name: 'Code Review Pipeline',
|
|
22
|
+
description: 'Analyze code, review for issues, then fix problems',
|
|
23
|
+
icon: '🔍',
|
|
24
|
+
agents: [
|
|
25
|
+
{
|
|
26
|
+
name: 'Code Analyzer',
|
|
27
|
+
model: 'sonnet',
|
|
28
|
+
prompt: 'Analyze the codebase structure and identify the main components, patterns, and potential areas of concern. Create a summary of findings.',
|
|
29
|
+
dependencies: []
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
name: 'Code Reviewer',
|
|
33
|
+
model: 'sonnet',
|
|
34
|
+
prompt: 'Review the code for bugs, security issues, performance problems, and code quality. List all issues found with severity ratings.',
|
|
35
|
+
dependencies: [] // Will be set to depend on Analyzer
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
name: 'Code Fixer',
|
|
39
|
+
model: 'sonnet',
|
|
40
|
+
prompt: 'Based on the review findings, fix the identified issues. Make the necessary code changes and explain what was fixed.',
|
|
41
|
+
dependencies: [] // Will be set to depend on Reviewer
|
|
42
|
+
}
|
|
43
|
+
]
|
|
44
|
+
},
|
|
45
|
+
'full-stack-feature': {
|
|
46
|
+
id: 'full-stack-feature',
|
|
47
|
+
name: 'Full-Stack Feature',
|
|
48
|
+
description: 'Build frontend, backend, and tests in parallel, then integrate',
|
|
49
|
+
icon: '🏗️',
|
|
50
|
+
agents: [
|
|
51
|
+
{
|
|
52
|
+
name: 'Frontend Developer',
|
|
53
|
+
model: 'sonnet',
|
|
54
|
+
prompt: 'Implement the frontend components for this feature. Create the UI, handle user interactions, and manage state.',
|
|
55
|
+
dependencies: []
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
name: 'Backend Developer',
|
|
59
|
+
model: 'sonnet',
|
|
60
|
+
prompt: 'Implement the backend API endpoints for this feature. Create the routes, controllers, and data models.',
|
|
61
|
+
dependencies: []
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
name: 'Test Engineer',
|
|
65
|
+
model: 'haiku',
|
|
66
|
+
prompt: 'Write comprehensive tests for this feature including unit tests, integration tests, and edge cases.',
|
|
67
|
+
dependencies: []
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
name: 'Integration Lead',
|
|
71
|
+
model: 'sonnet',
|
|
72
|
+
prompt: 'Integrate the frontend and backend components. Ensure they work together correctly and fix any integration issues.',
|
|
73
|
+
dependencies: [] // Will depend on Frontend and Backend
|
|
74
|
+
}
|
|
75
|
+
]
|
|
76
|
+
},
|
|
77
|
+
'documentation': {
|
|
78
|
+
id: 'documentation',
|
|
79
|
+
name: 'Documentation Generator',
|
|
80
|
+
description: 'Read code, write documentation, then review for quality',
|
|
81
|
+
icon: '📚',
|
|
82
|
+
agents: [
|
|
83
|
+
{
|
|
84
|
+
name: 'Code Reader',
|
|
85
|
+
model: 'haiku',
|
|
86
|
+
prompt: 'Read and understand the codebase. Identify all public APIs, functions, classes, and their purposes.',
|
|
87
|
+
dependencies: []
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
name: 'Doc Writer',
|
|
91
|
+
model: 'sonnet',
|
|
92
|
+
prompt: 'Write comprehensive documentation including API reference, usage examples, and architecture overview.',
|
|
93
|
+
dependencies: [] // Will depend on Code Reader
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
name: 'Doc Reviewer',
|
|
97
|
+
model: 'haiku',
|
|
98
|
+
prompt: 'Review the documentation for accuracy, clarity, and completeness. Suggest improvements.',
|
|
99
|
+
dependencies: [] // Will depend on Doc Writer
|
|
100
|
+
}
|
|
101
|
+
]
|
|
102
|
+
},
|
|
103
|
+
'bug-fix': {
|
|
104
|
+
id: 'bug-fix',
|
|
105
|
+
name: 'Bug Investigation & Fix',
|
|
106
|
+
description: 'Investigate bug, propose fix, implement and test',
|
|
107
|
+
icon: '🐛',
|
|
108
|
+
agents: [
|
|
109
|
+
{
|
|
110
|
+
name: 'Bug Investigator',
|
|
111
|
+
model: 'sonnet',
|
|
112
|
+
prompt: 'Investigate the reported bug. Find the root cause by examining logs, code, and reproducing the issue.',
|
|
113
|
+
dependencies: []
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
name: 'Fix Implementer',
|
|
117
|
+
model: 'sonnet',
|
|
118
|
+
prompt: 'Implement a fix for the bug based on the investigation findings. Make the necessary code changes.',
|
|
119
|
+
dependencies: [] // Will depend on Investigator
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
name: 'Test Verifier',
|
|
123
|
+
model: 'haiku',
|
|
124
|
+
prompt: 'Write tests to verify the bug is fixed and add regression tests to prevent future occurrences.',
|
|
125
|
+
dependencies: [] // Will depend on Implementer
|
|
126
|
+
}
|
|
127
|
+
]
|
|
128
|
+
},
|
|
129
|
+
'refactor': {
|
|
130
|
+
id: 'refactor',
|
|
131
|
+
name: 'Code Refactoring',
|
|
132
|
+
description: 'Analyze, plan, and execute a code refactoring',
|
|
133
|
+
icon: '♻️',
|
|
134
|
+
agents: [
|
|
135
|
+
{
|
|
136
|
+
name: 'Architecture Analyst',
|
|
137
|
+
model: 'sonnet',
|
|
138
|
+
prompt: 'Analyze the current code architecture and identify areas that need refactoring. Document technical debt.',
|
|
139
|
+
dependencies: []
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
name: 'Refactor Planner',
|
|
143
|
+
model: 'sonnet',
|
|
144
|
+
prompt: 'Create a detailed refactoring plan with step-by-step changes that maintain backward compatibility.',
|
|
145
|
+
dependencies: [] // Will depend on Analyst
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
name: 'Refactor Executor',
|
|
149
|
+
model: 'sonnet',
|
|
150
|
+
prompt: 'Execute the refactoring plan. Make incremental changes and ensure tests pass after each change.',
|
|
151
|
+
dependencies: [] // Will depend on Planner
|
|
152
|
+
}
|
|
153
|
+
]
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Get all available templates
|
|
159
|
+
* @returns {Array} Array of template summaries
|
|
160
|
+
*/
|
|
161
|
+
function getTemplates() {
|
|
162
|
+
return Object.values(ORCHESTRATION_TEMPLATES).map(t => ({
|
|
163
|
+
id: t.id,
|
|
164
|
+
name: t.name,
|
|
165
|
+
description: t.description,
|
|
166
|
+
icon: t.icon,
|
|
167
|
+
agentCount: t.agents.length
|
|
168
|
+
}));
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Get a specific template by ID
|
|
173
|
+
* @param {string} templateId - Template ID
|
|
174
|
+
* @returns {Object|null} Template or null
|
|
175
|
+
*/
|
|
176
|
+
function getTemplate(templateId) {
|
|
177
|
+
return ORCHESTRATION_TEMPLATES[templateId] || null;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Create orchestration from template
|
|
182
|
+
* @param {string} templateId - Template ID
|
|
183
|
+
* @param {Object} options - Override options
|
|
184
|
+
* @returns {Object} Result with orchestration or error
|
|
185
|
+
*/
|
|
186
|
+
function createFromTemplate(templateId, options = {}) {
|
|
187
|
+
const template = getTemplate(templateId);
|
|
188
|
+
|
|
189
|
+
if (!template) {
|
|
190
|
+
return { error: 'Template not found' };
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Create orchestration config from template
|
|
194
|
+
const config = {
|
|
195
|
+
name: options.name || template.name,
|
|
196
|
+
description: options.description || template.description,
|
|
197
|
+
templateId: templateId,
|
|
198
|
+
agents: []
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
// Create agents and set up dependencies
|
|
202
|
+
const agentIdMap = new Map(); // Map from index to generated ID
|
|
203
|
+
|
|
204
|
+
// First pass: create agents
|
|
205
|
+
template.agents.forEach((agentConfig, index) => {
|
|
206
|
+
const agent = {
|
|
207
|
+
name: agentConfig.name,
|
|
208
|
+
model: agentConfig.model,
|
|
209
|
+
prompt: options.customPrompts?.[index] || agentConfig.prompt,
|
|
210
|
+
workingDirectory: options.workingDirectory || process.cwd(),
|
|
211
|
+
dependencies: []
|
|
212
|
+
};
|
|
213
|
+
config.agents.push(agent);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
// Second pass: set up dependencies based on template structure
|
|
217
|
+
// For sequential templates, each agent depends on the previous
|
|
218
|
+
if (['code-review', 'documentation', 'bug-fix', 'refactor'].includes(templateId)) {
|
|
219
|
+
for (let i = 1; i < config.agents.length; i++) {
|
|
220
|
+
// Dependency will be resolved after agents are created
|
|
221
|
+
config.agents[i]._dependsOnIndex = i - 1;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// For full-stack-feature, the Integration Lead depends on Frontend and Backend
|
|
226
|
+
if (templateId === 'full-stack-feature') {
|
|
227
|
+
config.agents[3]._dependsOnIndex = [0, 1]; // Integration depends on Frontend and Backend
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Create the orchestration
|
|
231
|
+
const result = createOrchestration(config);
|
|
232
|
+
|
|
233
|
+
if (result.error) {
|
|
234
|
+
return result;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Now set up dependencies using actual agent IDs
|
|
238
|
+
const orch = result.orchestration;
|
|
239
|
+
const updates = [];
|
|
240
|
+
|
|
241
|
+
orch.agents.forEach((agent, index) => {
|
|
242
|
+
const templateAgent = config.agents[index];
|
|
243
|
+
if (templateAgent._dependsOnIndex !== undefined) {
|
|
244
|
+
const depIndexes = Array.isArray(templateAgent._dependsOnIndex)
|
|
245
|
+
? templateAgent._dependsOnIndex
|
|
246
|
+
: [templateAgent._dependsOnIndex];
|
|
247
|
+
|
|
248
|
+
depIndexes.forEach(depIndex => {
|
|
249
|
+
const depAgentId = orch.agents[depIndex]?.id;
|
|
250
|
+
if (depAgentId) {
|
|
251
|
+
addAgentDependency(orch.id, agent.id, depAgentId);
|
|
252
|
+
}
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
// Reload the orchestration to get updated dependencies
|
|
258
|
+
return { success: true, orchestration: getOrchestration(orch.id) };
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Generate a unique ID for orchestrations and agents
|
|
263
|
+
* @param {string} prefix - Prefix for the ID
|
|
264
|
+
* @returns {string} Unique ID
|
|
265
|
+
*/
|
|
266
|
+
function generateId(prefix = 'orch') {
|
|
267
|
+
return `${prefix}-${crypto.randomBytes(4).toString('hex')}`;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Ensure orchestrations directory exists
|
|
272
|
+
*/
|
|
273
|
+
function ensureDir() {
|
|
274
|
+
if (!fs.existsSync(ORCHESTRATIONS_DIR)) {
|
|
275
|
+
fs.mkdirSync(ORCHESTRATIONS_DIR, { recursive: true });
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Get the file path for an orchestration
|
|
281
|
+
* @param {string} orchestrationId - Orchestration ID
|
|
282
|
+
* @returns {string} File path
|
|
283
|
+
*/
|
|
284
|
+
function getOrchestrationPath(orchestrationId) {
|
|
285
|
+
return path.join(ORCHESTRATIONS_DIR, `${orchestrationId}.json`);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Create a new agent object
|
|
290
|
+
* @param {Object} config - Agent configuration
|
|
291
|
+
* @returns {Object} Agent object
|
|
292
|
+
*/
|
|
293
|
+
function createAgentObject(config) {
|
|
294
|
+
return {
|
|
295
|
+
id: generateId('agent'),
|
|
296
|
+
name: config.name || 'Unnamed Agent',
|
|
297
|
+
type: config.type || 'general-purpose', // general-purpose, explore, plan, custom
|
|
298
|
+
status: AGENT_STATUS.PENDING,
|
|
299
|
+
prompt: config.prompt || '',
|
|
300
|
+
model: config.model || 'sonnet', // haiku, sonnet, opus
|
|
301
|
+
workingDirectory: config.workingDirectory || process.cwd(),
|
|
302
|
+
sessionId: null, // Will be set when agent is spawned
|
|
303
|
+
tmuxWindow: null, // tmux window name
|
|
304
|
+
parentAgentId: config.parentAgentId || null,
|
|
305
|
+
dependencies: config.dependencies || [], // Agent IDs this agent depends on
|
|
306
|
+
createdAt: new Date().toISOString(),
|
|
307
|
+
startedAt: null,
|
|
308
|
+
completedAt: null,
|
|
309
|
+
error: null,
|
|
310
|
+
output: null,
|
|
311
|
+
toolsUsed: [],
|
|
312
|
+
metadata: config.metadata || {}
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Create a new orchestration
|
|
318
|
+
* @param {Object} config - Orchestration configuration
|
|
319
|
+
* @returns {Object} Result with orchestration or error
|
|
320
|
+
*/
|
|
321
|
+
function createOrchestration(config) {
|
|
322
|
+
ensureDir();
|
|
323
|
+
|
|
324
|
+
const orchestration = {
|
|
325
|
+
id: generateId('orch'),
|
|
326
|
+
name: config.name || 'Unnamed Orchestration',
|
|
327
|
+
description: config.description || '',
|
|
328
|
+
status: ORCHESTRATION_STATUS.DRAFT,
|
|
329
|
+
agents: [],
|
|
330
|
+
createdAt: new Date().toISOString(),
|
|
331
|
+
startedAt: null,
|
|
332
|
+
completedAt: null,
|
|
333
|
+
templateId: config.templateId || null,
|
|
334
|
+
metadata: config.metadata || {}
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
// Add initial agents if provided
|
|
338
|
+
if (config.agents && Array.isArray(config.agents)) {
|
|
339
|
+
for (const agentConfig of config.agents) {
|
|
340
|
+
orchestration.agents.push(createAgentObject(agentConfig));
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
try {
|
|
345
|
+
const filePath = getOrchestrationPath(orchestration.id);
|
|
346
|
+
fs.writeFileSync(filePath, JSON.stringify(orchestration, null, 2));
|
|
347
|
+
|
|
348
|
+
eventBus.emit(EventTypes.ORCHESTRATION_CREATED, { orchestration });
|
|
349
|
+
|
|
350
|
+
return { success: true, orchestration };
|
|
351
|
+
} catch (e) {
|
|
352
|
+
return { error: e.message };
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Get an orchestration by ID
|
|
358
|
+
* @param {string} orchestrationId - Orchestration ID
|
|
359
|
+
* @returns {Object|null} Orchestration object or null
|
|
360
|
+
*/
|
|
361
|
+
function getOrchestration(orchestrationId) {
|
|
362
|
+
const filePath = getOrchestrationPath(orchestrationId);
|
|
363
|
+
|
|
364
|
+
if (!fs.existsSync(filePath)) {
|
|
365
|
+
return null;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
try {
|
|
369
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
370
|
+
return JSON.parse(content);
|
|
371
|
+
} catch (e) {
|
|
372
|
+
console.error(`Error reading orchestration ${orchestrationId}:`, e.message);
|
|
373
|
+
return null;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* List all orchestrations
|
|
379
|
+
* @returns {Array} Array of orchestration objects
|
|
380
|
+
*/
|
|
381
|
+
function listOrchestrations() {
|
|
382
|
+
ensureDir();
|
|
383
|
+
|
|
384
|
+
const orchestrations = [];
|
|
385
|
+
|
|
386
|
+
try {
|
|
387
|
+
const files = fs.readdirSync(ORCHESTRATIONS_DIR).filter(f => f.endsWith('.json'));
|
|
388
|
+
|
|
389
|
+
for (const file of files) {
|
|
390
|
+
try {
|
|
391
|
+
const content = fs.readFileSync(path.join(ORCHESTRATIONS_DIR, file), 'utf-8');
|
|
392
|
+
const orchestration = JSON.parse(content);
|
|
393
|
+
orchestrations.push(orchestration);
|
|
394
|
+
} catch (e) {
|
|
395
|
+
console.error(`Error reading ${file}:`, e.message);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// Sort by creation date, newest first
|
|
400
|
+
orchestrations.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt));
|
|
401
|
+
} catch (e) {
|
|
402
|
+
console.error('Error listing orchestrations:', e.message);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
return orchestrations;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* Update an orchestration
|
|
410
|
+
* @param {string} orchestrationId - Orchestration ID
|
|
411
|
+
* @param {Object} updates - Updates to apply
|
|
412
|
+
* @returns {Object} Result with updated orchestration or error
|
|
413
|
+
*/
|
|
414
|
+
function updateOrchestration(orchestrationId, updates) {
|
|
415
|
+
const orchestration = getOrchestration(orchestrationId);
|
|
416
|
+
|
|
417
|
+
if (!orchestration) {
|
|
418
|
+
return { error: 'Orchestration not found' };
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
const previousStatus = orchestration.status;
|
|
422
|
+
|
|
423
|
+
// Apply updates
|
|
424
|
+
if (updates.name !== undefined) orchestration.name = updates.name;
|
|
425
|
+
if (updates.description !== undefined) orchestration.description = updates.description;
|
|
426
|
+
if (updates.status !== undefined) orchestration.status = updates.status;
|
|
427
|
+
if (updates.startedAt !== undefined) orchestration.startedAt = updates.startedAt;
|
|
428
|
+
if (updates.completedAt !== undefined) orchestration.completedAt = updates.completedAt;
|
|
429
|
+
if (updates.metadata !== undefined) {
|
|
430
|
+
orchestration.metadata = { ...orchestration.metadata, ...updates.metadata };
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
try {
|
|
434
|
+
const filePath = getOrchestrationPath(orchestrationId);
|
|
435
|
+
fs.writeFileSync(filePath, JSON.stringify(orchestration, null, 2));
|
|
436
|
+
|
|
437
|
+
eventBus.emit(EventTypes.ORCHESTRATION_UPDATED, {
|
|
438
|
+
orchestrationId,
|
|
439
|
+
orchestration,
|
|
440
|
+
updates,
|
|
441
|
+
previousStatus
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
// Emit specific status events
|
|
445
|
+
if (updates.status && updates.status !== previousStatus) {
|
|
446
|
+
switch (updates.status) {
|
|
447
|
+
case ORCHESTRATION_STATUS.RUNNING:
|
|
448
|
+
eventBus.emit(EventTypes.ORCHESTRATION_STARTED, { orchestrationId, orchestration });
|
|
449
|
+
break;
|
|
450
|
+
case ORCHESTRATION_STATUS.COMPLETED:
|
|
451
|
+
eventBus.emit(EventTypes.ORCHESTRATION_COMPLETED, { orchestrationId, orchestration });
|
|
452
|
+
break;
|
|
453
|
+
case ORCHESTRATION_STATUS.FAILED:
|
|
454
|
+
eventBus.emit(EventTypes.ORCHESTRATION_FAILED, { orchestrationId, orchestration });
|
|
455
|
+
break;
|
|
456
|
+
case ORCHESTRATION_STATUS.PAUSED:
|
|
457
|
+
eventBus.emit(EventTypes.ORCHESTRATION_PAUSED, { orchestrationId, orchestration });
|
|
458
|
+
break;
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
return { success: true, orchestration };
|
|
463
|
+
} catch (e) {
|
|
464
|
+
return { error: e.message };
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/**
|
|
469
|
+
* Delete an orchestration
|
|
470
|
+
* @param {string} orchestrationId - Orchestration ID
|
|
471
|
+
* @returns {Object} Result with success flag or error
|
|
472
|
+
*/
|
|
473
|
+
function deleteOrchestration(orchestrationId) {
|
|
474
|
+
const filePath = getOrchestrationPath(orchestrationId);
|
|
475
|
+
|
|
476
|
+
if (!fs.existsSync(filePath)) {
|
|
477
|
+
return { error: 'Orchestration not found' };
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
try {
|
|
481
|
+
const orchestration = getOrchestration(orchestrationId);
|
|
482
|
+
fs.unlinkSync(filePath);
|
|
483
|
+
|
|
484
|
+
eventBus.emit(EventTypes.ORCHESTRATION_DELETED, { orchestrationId, orchestration });
|
|
485
|
+
|
|
486
|
+
return { success: true };
|
|
487
|
+
} catch (e) {
|
|
488
|
+
return { error: e.message };
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
/**
|
|
493
|
+
* Add an agent to an orchestration
|
|
494
|
+
* @param {string} orchestrationId - Orchestration ID
|
|
495
|
+
* @param {Object} agentConfig - Agent configuration
|
|
496
|
+
* @returns {Object} Result with agent or error
|
|
497
|
+
*/
|
|
498
|
+
function addAgent(orchestrationId, agentConfig) {
|
|
499
|
+
const orchestration = getOrchestration(orchestrationId);
|
|
500
|
+
|
|
501
|
+
if (!orchestration) {
|
|
502
|
+
return { error: 'Orchestration not found' };
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
if (orchestration.status === ORCHESTRATION_STATUS.COMPLETED) {
|
|
506
|
+
return { error: 'Cannot add agents to a completed orchestration' };
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
const agent = createAgentObject(agentConfig);
|
|
510
|
+
orchestration.agents.push(agent);
|
|
511
|
+
|
|
512
|
+
try {
|
|
513
|
+
const filePath = getOrchestrationPath(orchestrationId);
|
|
514
|
+
fs.writeFileSync(filePath, JSON.stringify(orchestration, null, 2));
|
|
515
|
+
|
|
516
|
+
eventBus.emit(EventTypes.AGENT_CREATED, { orchestrationId, agent });
|
|
517
|
+
|
|
518
|
+
return { success: true, agent };
|
|
519
|
+
} catch (e) {
|
|
520
|
+
return { error: e.message };
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
/**
|
|
525
|
+
* Get an agent from an orchestration
|
|
526
|
+
* @param {string} orchestrationId - Orchestration ID
|
|
527
|
+
* @param {string} agentId - Agent ID
|
|
528
|
+
* @returns {Object|null} Agent object or null
|
|
529
|
+
*/
|
|
530
|
+
function getAgent(orchestrationId, agentId) {
|
|
531
|
+
const orchestration = getOrchestration(orchestrationId);
|
|
532
|
+
|
|
533
|
+
if (!orchestration) {
|
|
534
|
+
return null;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
return orchestration.agents.find(a => a.id === agentId) || null;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
/**
|
|
541
|
+
* Update an agent within an orchestration
|
|
542
|
+
* @param {string} orchestrationId - Orchestration ID
|
|
543
|
+
* @param {string} agentId - Agent ID
|
|
544
|
+
* @param {Object} updates - Updates to apply
|
|
545
|
+
* @returns {Object} Result with updated agent or error
|
|
546
|
+
*/
|
|
547
|
+
function updateAgent(orchestrationId, agentId, updates) {
|
|
548
|
+
const orchestration = getOrchestration(orchestrationId);
|
|
549
|
+
|
|
550
|
+
if (!orchestration) {
|
|
551
|
+
return { error: 'Orchestration not found' };
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
const agentIndex = orchestration.agents.findIndex(a => a.id === agentId);
|
|
555
|
+
|
|
556
|
+
if (agentIndex === -1) {
|
|
557
|
+
return { error: 'Agent not found' };
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
const agent = orchestration.agents[agentIndex];
|
|
561
|
+
const previousStatus = agent.status;
|
|
562
|
+
|
|
563
|
+
// Apply updates
|
|
564
|
+
if (updates.name !== undefined) agent.name = updates.name;
|
|
565
|
+
if (updates.status !== undefined) agent.status = updates.status;
|
|
566
|
+
if (updates.prompt !== undefined) agent.prompt = updates.prompt;
|
|
567
|
+
if (updates.sessionId !== undefined) agent.sessionId = updates.sessionId;
|
|
568
|
+
if (updates.tmuxWindow !== undefined) agent.tmuxWindow = updates.tmuxWindow;
|
|
569
|
+
if (updates.startedAt !== undefined) agent.startedAt = updates.startedAt;
|
|
570
|
+
if (updates.completedAt !== undefined) agent.completedAt = updates.completedAt;
|
|
571
|
+
if (updates.error !== undefined) agent.error = updates.error;
|
|
572
|
+
if (updates.output !== undefined) agent.output = updates.output;
|
|
573
|
+
if (updates.toolsUsed !== undefined) agent.toolsUsed = updates.toolsUsed;
|
|
574
|
+
if (updates.metadata !== undefined) {
|
|
575
|
+
agent.metadata = { ...agent.metadata, ...updates.metadata };
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
orchestration.agents[agentIndex] = agent;
|
|
579
|
+
|
|
580
|
+
try {
|
|
581
|
+
const filePath = getOrchestrationPath(orchestrationId);
|
|
582
|
+
fs.writeFileSync(filePath, JSON.stringify(orchestration, null, 2));
|
|
583
|
+
|
|
584
|
+
eventBus.emit(EventTypes.AGENT_STATUS_CHANGED, {
|
|
585
|
+
orchestrationId,
|
|
586
|
+
agentId,
|
|
587
|
+
agent,
|
|
588
|
+
updates,
|
|
589
|
+
previousStatus
|
|
590
|
+
});
|
|
591
|
+
|
|
592
|
+
// Emit specific agent events
|
|
593
|
+
if (updates.status && updates.status !== previousStatus) {
|
|
594
|
+
switch (updates.status) {
|
|
595
|
+
case AGENT_STATUS.RUNNING:
|
|
596
|
+
eventBus.emit(EventTypes.AGENT_SPAWNED, { orchestrationId, agent });
|
|
597
|
+
break;
|
|
598
|
+
case AGENT_STATUS.COMPLETED:
|
|
599
|
+
eventBus.emit(EventTypes.AGENT_COMPLETED, { orchestrationId, agent });
|
|
600
|
+
break;
|
|
601
|
+
case AGENT_STATUS.FAILED:
|
|
602
|
+
eventBus.emit(EventTypes.AGENT_FAILED, { orchestrationId, agent });
|
|
603
|
+
break;
|
|
604
|
+
case AGENT_STATUS.CANCELLED:
|
|
605
|
+
eventBus.emit(EventTypes.AGENT_KILLED, { orchestrationId, agent });
|
|
606
|
+
break;
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
return { success: true, agent };
|
|
611
|
+
} catch (e) {
|
|
612
|
+
return { error: e.message };
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
/**
|
|
617
|
+
* Remove an agent from an orchestration
|
|
618
|
+
* @param {string} orchestrationId - Orchestration ID
|
|
619
|
+
* @param {string} agentId - Agent ID
|
|
620
|
+
* @returns {Object} Result with success flag or error
|
|
621
|
+
*/
|
|
622
|
+
function removeAgent(orchestrationId, agentId) {
|
|
623
|
+
const orchestration = getOrchestration(orchestrationId);
|
|
624
|
+
|
|
625
|
+
if (!orchestration) {
|
|
626
|
+
return { error: 'Orchestration not found' };
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
const agentIndex = orchestration.agents.findIndex(a => a.id === agentId);
|
|
630
|
+
|
|
631
|
+
if (agentIndex === -1) {
|
|
632
|
+
return { error: 'Agent not found' };
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
const agent = orchestration.agents[agentIndex];
|
|
636
|
+
|
|
637
|
+
// Check if agent is running - can't remove running agents
|
|
638
|
+
if (agent.status === AGENT_STATUS.RUNNING || agent.status === AGENT_STATUS.SPAWNING) {
|
|
639
|
+
return { error: 'Cannot remove a running agent. Kill it first.' };
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
// Remove this agent from dependencies of other agents
|
|
643
|
+
for (const otherAgent of orchestration.agents) {
|
|
644
|
+
if (otherAgent.dependencies.includes(agentId)) {
|
|
645
|
+
otherAgent.dependencies = otherAgent.dependencies.filter(id => id !== agentId);
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
orchestration.agents.splice(agentIndex, 1);
|
|
650
|
+
|
|
651
|
+
try {
|
|
652
|
+
const filePath = getOrchestrationPath(orchestrationId);
|
|
653
|
+
fs.writeFileSync(filePath, JSON.stringify(orchestration, null, 2));
|
|
654
|
+
|
|
655
|
+
eventBus.emit(EventTypes.AGENT_KILLED, { orchestrationId, agent, removed: true });
|
|
656
|
+
|
|
657
|
+
return { success: true };
|
|
658
|
+
} catch (e) {
|
|
659
|
+
return { error: e.message };
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
/**
|
|
664
|
+
* Add a dependency between agents
|
|
665
|
+
* @param {string} orchestrationId - Orchestration ID
|
|
666
|
+
* @param {string} agentId - Agent ID (the one that will be blocked)
|
|
667
|
+
* @param {string} dependsOnAgentId - Agent ID to depend on (the blocker)
|
|
668
|
+
* @returns {Object} Result with success flag or error
|
|
669
|
+
*/
|
|
670
|
+
function addAgentDependency(orchestrationId, agentId, dependsOnAgentId) {
|
|
671
|
+
const orchestration = getOrchestration(orchestrationId);
|
|
672
|
+
|
|
673
|
+
if (!orchestration) {
|
|
674
|
+
return { error: 'Orchestration not found' };
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
const agent = orchestration.agents.find(a => a.id === agentId);
|
|
678
|
+
const dependsOnAgent = orchestration.agents.find(a => a.id === dependsOnAgentId);
|
|
679
|
+
|
|
680
|
+
if (!agent || !dependsOnAgent) {
|
|
681
|
+
return { error: 'Agent not found' };
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
if (agentId === dependsOnAgentId) {
|
|
685
|
+
return { error: 'Agent cannot depend on itself' };
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
// Check for circular dependencies
|
|
689
|
+
if (wouldCreateCycle(orchestration, agentId, dependsOnAgentId)) {
|
|
690
|
+
return { error: 'This would create a circular dependency' };
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
if (!agent.dependencies.includes(dependsOnAgentId)) {
|
|
694
|
+
agent.dependencies.push(dependsOnAgentId);
|
|
695
|
+
|
|
696
|
+
try {
|
|
697
|
+
const filePath = getOrchestrationPath(orchestrationId);
|
|
698
|
+
fs.writeFileSync(filePath, JSON.stringify(orchestration, null, 2));
|
|
699
|
+
|
|
700
|
+
eventBus.emit(EventTypes.ORCHESTRATION_UPDATED, {
|
|
701
|
+
orchestrationId,
|
|
702
|
+
orchestration,
|
|
703
|
+
updates: { dependencyAdded: { agentId, dependsOnAgentId } }
|
|
704
|
+
});
|
|
705
|
+
|
|
706
|
+
return { success: true };
|
|
707
|
+
} catch (e) {
|
|
708
|
+
return { error: e.message };
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
return { success: true }; // Already exists
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
/**
|
|
716
|
+
* Remove a dependency between agents
|
|
717
|
+
* @param {string} orchestrationId - Orchestration ID
|
|
718
|
+
* @param {string} agentId - Agent ID
|
|
719
|
+
* @param {string} dependsOnAgentId - Agent ID to remove from dependencies
|
|
720
|
+
* @returns {Object} Result with success flag or error
|
|
721
|
+
*/
|
|
722
|
+
function removeAgentDependency(orchestrationId, agentId, dependsOnAgentId) {
|
|
723
|
+
const orchestration = getOrchestration(orchestrationId);
|
|
724
|
+
|
|
725
|
+
if (!orchestration) {
|
|
726
|
+
return { error: 'Orchestration not found' };
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
const agent = orchestration.agents.find(a => a.id === agentId);
|
|
730
|
+
|
|
731
|
+
if (!agent) {
|
|
732
|
+
return { error: 'Agent not found' };
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
agent.dependencies = agent.dependencies.filter(id => id !== dependsOnAgentId);
|
|
736
|
+
|
|
737
|
+
try {
|
|
738
|
+
const filePath = getOrchestrationPath(orchestrationId);
|
|
739
|
+
fs.writeFileSync(filePath, JSON.stringify(orchestration, null, 2));
|
|
740
|
+
|
|
741
|
+
eventBus.emit(EventTypes.ORCHESTRATION_UPDATED, {
|
|
742
|
+
orchestrationId,
|
|
743
|
+
orchestration,
|
|
744
|
+
updates: { dependencyRemoved: { agentId, dependsOnAgentId } }
|
|
745
|
+
});
|
|
746
|
+
|
|
747
|
+
return { success: true };
|
|
748
|
+
} catch (e) {
|
|
749
|
+
return { error: e.message };
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
/**
|
|
754
|
+
* Check if adding a dependency would create a cycle
|
|
755
|
+
* @param {Object} orchestration - Orchestration object
|
|
756
|
+
* @param {string} agentId - Agent that would get the dependency
|
|
757
|
+
* @param {string} dependsOnAgentId - The dependency to add
|
|
758
|
+
* @returns {boolean} True if it would create a cycle
|
|
759
|
+
*/
|
|
760
|
+
function wouldCreateCycle(orchestration, agentId, dependsOnAgentId) {
|
|
761
|
+
const visited = new Set();
|
|
762
|
+
const stack = [dependsOnAgentId];
|
|
763
|
+
|
|
764
|
+
while (stack.length > 0) {
|
|
765
|
+
const current = stack.pop();
|
|
766
|
+
|
|
767
|
+
if (current === agentId) {
|
|
768
|
+
return true; // Found a cycle
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
if (visited.has(current)) {
|
|
772
|
+
continue;
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
visited.add(current);
|
|
776
|
+
|
|
777
|
+
const agent = orchestration.agents.find(a => a.id === current);
|
|
778
|
+
if (agent) {
|
|
779
|
+
stack.push(...agent.dependencies);
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
return false;
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
/**
|
|
787
|
+
* Get agents that are ready to run (no pending dependencies)
|
|
788
|
+
* @param {string} orchestrationId - Orchestration ID
|
|
789
|
+
* @returns {Array} Array of agents ready to run
|
|
790
|
+
*/
|
|
791
|
+
function getReadyAgents(orchestrationId) {
|
|
792
|
+
const orchestration = getOrchestration(orchestrationId);
|
|
793
|
+
|
|
794
|
+
if (!orchestration) {
|
|
795
|
+
return [];
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
return orchestration.agents.filter(agent => {
|
|
799
|
+
// Only pending agents can be ready
|
|
800
|
+
if (agent.status !== AGENT_STATUS.PENDING) {
|
|
801
|
+
return false;
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
// Check all dependencies are completed
|
|
805
|
+
for (const depId of agent.dependencies) {
|
|
806
|
+
const depAgent = orchestration.agents.find(a => a.id === depId);
|
|
807
|
+
if (!depAgent || depAgent.status !== AGENT_STATUS.COMPLETED) {
|
|
808
|
+
return false;
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
return true;
|
|
813
|
+
});
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
/**
|
|
817
|
+
* Get orchestration statistics
|
|
818
|
+
* @param {string} orchestrationId - Orchestration ID
|
|
819
|
+
* @returns {Object} Statistics object
|
|
820
|
+
*/
|
|
821
|
+
function getOrchestrationStats(orchestrationId) {
|
|
822
|
+
const orchestration = getOrchestration(orchestrationId);
|
|
823
|
+
|
|
824
|
+
if (!orchestration) {
|
|
825
|
+
return null;
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
const agents = orchestration.agents;
|
|
829
|
+
|
|
830
|
+
return {
|
|
831
|
+
total: agents.length,
|
|
832
|
+
pending: agents.filter(a => a.status === AGENT_STATUS.PENDING).length,
|
|
833
|
+
spawning: agents.filter(a => a.status === AGENT_STATUS.SPAWNING).length,
|
|
834
|
+
running: agents.filter(a => a.status === AGENT_STATUS.RUNNING).length,
|
|
835
|
+
waiting: agents.filter(a => a.status === AGENT_STATUS.WAITING).length,
|
|
836
|
+
completed: agents.filter(a => a.status === AGENT_STATUS.COMPLETED).length,
|
|
837
|
+
failed: agents.filter(a => a.status === AGENT_STATUS.FAILED).length,
|
|
838
|
+
cancelled: agents.filter(a => a.status === AGENT_STATUS.CANCELLED).length,
|
|
839
|
+
readyToRun: getReadyAgents(orchestrationId).length
|
|
840
|
+
};
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
/**
|
|
844
|
+
* Check if orchestration is complete (all agents done)
|
|
845
|
+
* @param {string} orchestrationId - Orchestration ID
|
|
846
|
+
* @returns {boolean} True if complete
|
|
847
|
+
*/
|
|
848
|
+
function isOrchestrationComplete(orchestrationId) {
|
|
849
|
+
const orchestration = getOrchestration(orchestrationId);
|
|
850
|
+
|
|
851
|
+
if (!orchestration || orchestration.agents.length === 0) {
|
|
852
|
+
return false;
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
const terminalStatuses = [
|
|
856
|
+
AGENT_STATUS.COMPLETED,
|
|
857
|
+
AGENT_STATUS.FAILED,
|
|
858
|
+
AGENT_STATUS.CANCELLED
|
|
859
|
+
];
|
|
860
|
+
|
|
861
|
+
return orchestration.agents.every(a => terminalStatuses.includes(a.status));
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
/**
|
|
865
|
+
* Record tool usage for an agent
|
|
866
|
+
* @param {string} orchestrationId - Orchestration ID
|
|
867
|
+
* @param {string} agentId - Agent ID
|
|
868
|
+
* @param {Object} toolData - Tool usage data
|
|
869
|
+
* @returns {Object} Result with success flag
|
|
870
|
+
*/
|
|
871
|
+
function recordAgentToolUsage(orchestrationId, agentId, toolData) {
|
|
872
|
+
const orchestration = getOrchestration(orchestrationId);
|
|
873
|
+
|
|
874
|
+
if (!orchestration) {
|
|
875
|
+
return { error: 'Orchestration not found' };
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
const agent = orchestration.agents.find(a => a.id === agentId);
|
|
879
|
+
|
|
880
|
+
if (!agent) {
|
|
881
|
+
return { error: 'Agent not found' };
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
agent.toolsUsed.push({
|
|
885
|
+
tool: toolData.tool,
|
|
886
|
+
timestamp: new Date().toISOString(),
|
|
887
|
+
duration: toolData.duration || null,
|
|
888
|
+
status: toolData.status || 'unknown'
|
|
889
|
+
});
|
|
890
|
+
|
|
891
|
+
try {
|
|
892
|
+
const filePath = getOrchestrationPath(orchestrationId);
|
|
893
|
+
fs.writeFileSync(filePath, JSON.stringify(orchestration, null, 2));
|
|
894
|
+
|
|
895
|
+
eventBus.emit(EventTypes.AGENT_OUTPUT, {
|
|
896
|
+
orchestrationId,
|
|
897
|
+
agentId,
|
|
898
|
+
type: 'tool_use',
|
|
899
|
+
data: toolData
|
|
900
|
+
});
|
|
901
|
+
|
|
902
|
+
return { success: true };
|
|
903
|
+
} catch (e) {
|
|
904
|
+
return { error: e.message };
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
module.exports = {
|
|
909
|
+
// Orchestration CRUD
|
|
910
|
+
createOrchestration,
|
|
911
|
+
getOrchestration,
|
|
912
|
+
listOrchestrations,
|
|
913
|
+
updateOrchestration,
|
|
914
|
+
deleteOrchestration,
|
|
915
|
+
|
|
916
|
+
// Agent CRUD
|
|
917
|
+
addAgent,
|
|
918
|
+
getAgent,
|
|
919
|
+
updateAgent,
|
|
920
|
+
removeAgent,
|
|
921
|
+
|
|
922
|
+
// Dependencies
|
|
923
|
+
addAgentDependency,
|
|
924
|
+
removeAgentDependency,
|
|
925
|
+
getReadyAgents,
|
|
926
|
+
|
|
927
|
+
// Templates
|
|
928
|
+
getTemplates,
|
|
929
|
+
getTemplate,
|
|
930
|
+
createFromTemplate,
|
|
931
|
+
|
|
932
|
+
// Utilities
|
|
933
|
+
getOrchestrationStats,
|
|
934
|
+
isOrchestrationComplete,
|
|
935
|
+
recordAgentToolUsage,
|
|
936
|
+
generateId,
|
|
937
|
+
|
|
938
|
+
// Constants re-export for convenience
|
|
939
|
+
AGENT_STATUS,
|
|
940
|
+
ORCHESTRATION_STATUS
|
|
941
|
+
};
|