lynkr 1.0.0 → 2.0.0
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/CITATIONS.bib +6 -0
- package/DEPLOYMENT.md +1001 -0
- package/README.md +215 -71
- package/docs/index.md +55 -2
- package/monitor-agents.sh +31 -0
- package/package.json +7 -3
- package/src/agents/context-manager.js +220 -0
- package/src/agents/definitions/loader.js +563 -0
- package/src/agents/executor.js +412 -0
- package/src/agents/index.js +157 -0
- package/src/agents/parallel-coordinator.js +68 -0
- package/src/agents/reflector.js +321 -0
- package/src/agents/skillbook.js +331 -0
- package/src/agents/store.js +244 -0
- package/src/api/router.js +55 -0
- package/src/clients/databricks.js +214 -17
- package/src/clients/routing.js +15 -7
- package/src/clients/standard-tools.js +341 -0
- package/src/config/index.js +41 -5
- package/src/orchestrator/index.js +254 -37
- package/src/server.js +2 -0
- package/src/tools/agent-task.js +96 -0
- package/test/azure-openai-config.test.js +203 -0
- package/test/azure-openai-error-resilience.test.js +238 -0
- package/test/azure-openai-format-conversion.test.js +354 -0
- package/test/azure-openai-integration.test.js +281 -0
- package/test/azure-openai-routing.test.js +148 -0
- package/test/azure-openai-streaming.test.js +171 -0
- package/test/format-conversion.test.js +578 -0
- package/test/hybrid-routing-integration.test.js +18 -11
- package/test/openrouter-error-resilience.test.js +418 -0
- package/test/passthrough-mode.test.js +385 -0
- package/test/routing.test.js +9 -3
- package/test/web-tools.test.js +3 -0
- package/test-agents-simple.js +43 -0
- package/test-cli-connection.sh +33 -0
- package/test-learning-unit.js +126 -0
- package/test-learning.js +112 -0
- package/test-parallel-agents.sh +124 -0
- package/test-parallel-direct.js +155 -0
- package/test-subagents.sh +117 -0
|
@@ -0,0 +1,412 @@
|
|
|
1
|
+
const logger = require("../logger");
|
|
2
|
+
const { executeToolCall, listTools } = require("../tools");
|
|
3
|
+
const { invokeModel } = require("../clients/databricks");
|
|
4
|
+
const { STANDARD_TOOLS } = require("../clients/standard-tools");
|
|
5
|
+
const ContextManager = require("./context-manager");
|
|
6
|
+
const Skillbook = require("./skillbook");
|
|
7
|
+
const Reflector = require("./reflector");
|
|
8
|
+
|
|
9
|
+
const contextManager = new ContextManager();
|
|
10
|
+
|
|
11
|
+
class SubagentExecutor {
|
|
12
|
+
/**
|
|
13
|
+
* Execute a single subagent
|
|
14
|
+
* @param {Object} agentDef - Agent definition
|
|
15
|
+
* @param {string} taskPrompt - Task to perform
|
|
16
|
+
* @param {Object} options - sessionId, mainContext, etc.
|
|
17
|
+
* @returns {Promise<Object>} - { success, result, stats }
|
|
18
|
+
*/
|
|
19
|
+
async execute(agentDef, taskPrompt, options = {}) {
|
|
20
|
+
// Create fresh isolated context
|
|
21
|
+
const context = contextManager.createSubagentContext(
|
|
22
|
+
agentDef,
|
|
23
|
+
taskPrompt,
|
|
24
|
+
options.mainContext
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
// Set timeout
|
|
29
|
+
const timeout = options.timeout || 120000; // 2 minutes
|
|
30
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
31
|
+
setTimeout(() => reject(new Error("Subagent timeout")), timeout);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
const executionPromise = this._runAgentLoop(context, options.sessionId);
|
|
35
|
+
|
|
36
|
+
await Promise.race([executionPromise, timeoutPromise]);
|
|
37
|
+
|
|
38
|
+
// Extract final result (summary only, not intermediate steps)
|
|
39
|
+
const finalResult = this._extractFinalResult(context);
|
|
40
|
+
|
|
41
|
+
contextManager.completeExecution(context, finalResult);
|
|
42
|
+
|
|
43
|
+
// Learn from successful execution (async, non-blocking)
|
|
44
|
+
this._learnFromExecution(context, true).catch(err => {
|
|
45
|
+
logger.warn({
|
|
46
|
+
agentType: agentDef.name,
|
|
47
|
+
error: err.message
|
|
48
|
+
}, "Failed to learn from execution");
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
success: true,
|
|
53
|
+
result: finalResult,
|
|
54
|
+
stats: {
|
|
55
|
+
agentId: context.agentId,
|
|
56
|
+
steps: context.steps,
|
|
57
|
+
durationMs: Date.now() - context.startTime,
|
|
58
|
+
inputTokens: context.inputTokens,
|
|
59
|
+
outputTokens: context.outputTokens
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
} catch (error) {
|
|
64
|
+
contextManager.failExecution(context, error);
|
|
65
|
+
|
|
66
|
+
// Learn from failed execution (async, non-blocking)
|
|
67
|
+
this._learnFromExecution(context, false).catch(err => {
|
|
68
|
+
logger.debug({
|
|
69
|
+
agentType: agentDef.name,
|
|
70
|
+
error: err.message
|
|
71
|
+
}, "Failed to learn from failed execution");
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
success: false,
|
|
76
|
+
error: error.message,
|
|
77
|
+
stats: {
|
|
78
|
+
agentId: context.agentId,
|
|
79
|
+
steps: context.steps,
|
|
80
|
+
durationMs: Date.now() - context.startTime
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Run agent loop (similar to main orchestrator but isolated)
|
|
88
|
+
*/
|
|
89
|
+
async _runAgentLoop(context, sessionId) {
|
|
90
|
+
while (context.steps < context.maxSteps && !context.terminated) {
|
|
91
|
+
context.steps++;
|
|
92
|
+
|
|
93
|
+
logger.debug({
|
|
94
|
+
agentId: context.agentId,
|
|
95
|
+
step: context.steps,
|
|
96
|
+
messageCount: context.messages.length
|
|
97
|
+
}, "Subagent step starting");
|
|
98
|
+
|
|
99
|
+
// Call model with filtered tools
|
|
100
|
+
const response = await this._callModel(context);
|
|
101
|
+
|
|
102
|
+
// Update token usage
|
|
103
|
+
context.inputTokens += response.usage?.input_tokens || 0;
|
|
104
|
+
context.outputTokens += response.usage?.output_tokens || 0;
|
|
105
|
+
|
|
106
|
+
// Check stop reason
|
|
107
|
+
if (response.stop_reason === "end_turn" || response.stop_reason === "stop_sequence") {
|
|
108
|
+
// Agent finished - extract result
|
|
109
|
+
context.result = this._extractTextFromContent(response.content);
|
|
110
|
+
context.terminated = true;
|
|
111
|
+
|
|
112
|
+
contextManager.addMessage(context, {
|
|
113
|
+
role: "assistant",
|
|
114
|
+
content: response.content
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
break;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Execute tool calls if any
|
|
121
|
+
if (response.stop_reason === "tool_use") {
|
|
122
|
+
await this._executeTools(context, response.content, sessionId);
|
|
123
|
+
} else {
|
|
124
|
+
logger.warn({
|
|
125
|
+
agentId: context.agentId,
|
|
126
|
+
stopReason: response.stop_reason
|
|
127
|
+
}, "Unexpected stop reason in subagent");
|
|
128
|
+
|
|
129
|
+
context.result = this._extractTextFromContent(response.content);
|
|
130
|
+
context.terminated = true;
|
|
131
|
+
break;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (context.steps >= context.maxSteps && !context.terminated) {
|
|
136
|
+
logger.warn({
|
|
137
|
+
agentId: context.agentId,
|
|
138
|
+
maxSteps: context.maxSteps
|
|
139
|
+
}, "Subagent reached max steps");
|
|
140
|
+
|
|
141
|
+
context.result = "Subagent incomplete - reached maximum steps";
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Call model with subagent context
|
|
147
|
+
*/
|
|
148
|
+
async _callModel(context) {
|
|
149
|
+
const payload = {
|
|
150
|
+
model: this._resolveModel(context.model),
|
|
151
|
+
messages: context.messages,
|
|
152
|
+
max_tokens: 4096,
|
|
153
|
+
temperature: 0.3
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
// Add filtered tools for subagent (based on allowedTools)
|
|
157
|
+
const filteredTools = this._getFilteredTools(context.allowedTools);
|
|
158
|
+
if (filteredTools.length > 0) {
|
|
159
|
+
payload.tools = filteredTools;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
logger.debug({
|
|
163
|
+
agentId: context.agentId,
|
|
164
|
+
model: payload.model,
|
|
165
|
+
messageCount: context.messages.length,
|
|
166
|
+
toolCount: filteredTools.length,
|
|
167
|
+
toolNames: filteredTools.map(t => t.name)
|
|
168
|
+
}, "Calling model for subagent");
|
|
169
|
+
|
|
170
|
+
// Use invokeModel to leverage provider routing
|
|
171
|
+
const response = await invokeModel(payload);
|
|
172
|
+
|
|
173
|
+
if (!response.json) {
|
|
174
|
+
throw new Error("Invalid model response");
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return response.json;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Execute tools (with restrictions)
|
|
182
|
+
*/
|
|
183
|
+
async _executeTools(context, content, sessionId) {
|
|
184
|
+
const toolUseBlocks = content.filter(block => block.type === "tool_use");
|
|
185
|
+
|
|
186
|
+
if (toolUseBlocks.length === 0) {
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Add assistant message with tool calls
|
|
191
|
+
contextManager.addMessage(context, {
|
|
192
|
+
role: "assistant",
|
|
193
|
+
content: content
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
// Execute each tool (sequentially for now, can parallelize later)
|
|
197
|
+
const toolResults = [];
|
|
198
|
+
|
|
199
|
+
for (const toolUse of toolUseBlocks) {
|
|
200
|
+
const toolStart = Date.now();
|
|
201
|
+
|
|
202
|
+
try {
|
|
203
|
+
// Check if tool is allowed
|
|
204
|
+
if (context.allowedTools.length > 0 && !this._isToolAllowed(toolUse.name, context.allowedTools)) {
|
|
205
|
+
throw new Error(`Tool ${toolUse.name} not allowed for agent ${context.agentName}`);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// CRITICAL: Block Task tool for subagents (prevents recursion)
|
|
209
|
+
if (toolUse.name === "Task") {
|
|
210
|
+
throw new Error("Subagents cannot spawn other subagents");
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
logger.debug({
|
|
214
|
+
agentId: context.agentId,
|
|
215
|
+
step: context.steps,
|
|
216
|
+
toolName: toolUse.name
|
|
217
|
+
}, "Subagent executing tool");
|
|
218
|
+
|
|
219
|
+
// Execute tool
|
|
220
|
+
const result = await executeToolCall({
|
|
221
|
+
name: toolUse.name,
|
|
222
|
+
arguments: toolUse.input
|
|
223
|
+
}, {
|
|
224
|
+
sessionId: sessionId,
|
|
225
|
+
agentId: context.agentId,
|
|
226
|
+
isSubagent: true
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
const toolDuration = Date.now() - toolStart;
|
|
230
|
+
|
|
231
|
+
// Record in transcript
|
|
232
|
+
contextManager.recordToolCall(
|
|
233
|
+
context,
|
|
234
|
+
toolUse.name,
|
|
235
|
+
toolUse.input,
|
|
236
|
+
result.content,
|
|
237
|
+
null
|
|
238
|
+
);
|
|
239
|
+
|
|
240
|
+
toolResults.push({
|
|
241
|
+
type: "tool_result",
|
|
242
|
+
tool_use_id: toolUse.id,
|
|
243
|
+
content: result.content
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
} catch (error) {
|
|
247
|
+
const toolDuration = Date.now() - toolStart;
|
|
248
|
+
|
|
249
|
+
logger.warn({
|
|
250
|
+
agentId: context.agentId,
|
|
251
|
+
toolName: toolUse.name,
|
|
252
|
+
error: error.message
|
|
253
|
+
}, "Subagent tool execution failed");
|
|
254
|
+
|
|
255
|
+
contextManager.recordToolCall(
|
|
256
|
+
context,
|
|
257
|
+
toolUse.name,
|
|
258
|
+
toolUse.input,
|
|
259
|
+
null,
|
|
260
|
+
error
|
|
261
|
+
);
|
|
262
|
+
|
|
263
|
+
toolResults.push({
|
|
264
|
+
type: "tool_result",
|
|
265
|
+
tool_use_id: toolUse.id,
|
|
266
|
+
content: `Error: ${error.message}`,
|
|
267
|
+
is_error: true
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Add tool results as user message
|
|
273
|
+
contextManager.addMessage(context, {
|
|
274
|
+
role: "user",
|
|
275
|
+
content: toolResults
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Get filtered tools for subagent (based on allowedTools)
|
|
281
|
+
* Returns tools in Anthropic format (conversion to OpenAI happens in invokeModel)
|
|
282
|
+
*/
|
|
283
|
+
_getFilteredTools(allowedTools) {
|
|
284
|
+
if (!allowedTools || allowedTools.length === 0) {
|
|
285
|
+
return [];
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Filter STANDARD_TOOLS based on allowedTools list
|
|
289
|
+
// Exclude Task tool (subagents cannot spawn other subagents)
|
|
290
|
+
return STANDARD_TOOLS.filter(tool => {
|
|
291
|
+
if (tool.name === "Task") {
|
|
292
|
+
return false; // Never allow subagents to spawn subagents
|
|
293
|
+
}
|
|
294
|
+
return this._isToolAllowed(tool.name, allowedTools);
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Check if tool is allowed (case-insensitive)
|
|
300
|
+
*/
|
|
301
|
+
_isToolAllowed(toolName, allowedTools) {
|
|
302
|
+
const normalized = toolName.toLowerCase();
|
|
303
|
+
return allowedTools.some(allowed => allowed.toLowerCase() === normalized);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Extract text from content blocks
|
|
308
|
+
*/
|
|
309
|
+
_extractTextFromContent(content) {
|
|
310
|
+
if (!Array.isArray(content)) {
|
|
311
|
+
return String(content);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const textBlocks = content.filter(block => block.type === "text");
|
|
315
|
+
return textBlocks.map(block => block.text).join("\n");
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Extract FINAL RESULT only (not intermediate steps)
|
|
320
|
+
*/
|
|
321
|
+
_extractFinalResult(context) {
|
|
322
|
+
if (context.result) {
|
|
323
|
+
return context.result;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// Look for summary markers in last assistant message
|
|
327
|
+
const reversedMessages = [...context.messages].reverse();
|
|
328
|
+
const lastMessage = reversedMessages.find(m => m.role === "assistant");
|
|
329
|
+
|
|
330
|
+
if (lastMessage && lastMessage.content) {
|
|
331
|
+
const text = this._extractTextFromContent(lastMessage.content);
|
|
332
|
+
|
|
333
|
+
// Look for summary markers
|
|
334
|
+
const markers = [
|
|
335
|
+
"EXPLORATION COMPLETE:",
|
|
336
|
+
"IMPLEMENTATION PLAN:",
|
|
337
|
+
"TASK COMPLETE:",
|
|
338
|
+
"SUMMARY:",
|
|
339
|
+
"FINDINGS:"
|
|
340
|
+
];
|
|
341
|
+
|
|
342
|
+
for (const marker of markers) {
|
|
343
|
+
const index = text.indexOf(marker);
|
|
344
|
+
if (index !== -1) {
|
|
345
|
+
return text.substring(index);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
return text;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
return "Subagent completed with no result";
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Resolve model name to full model identifier
|
|
357
|
+
*/
|
|
358
|
+
_resolveModel(modelName) {
|
|
359
|
+
const modelMap = {
|
|
360
|
+
"haiku": "claude-3-haiku-20240307",
|
|
361
|
+
"sonnet": "claude-3-5-sonnet-20241022",
|
|
362
|
+
"opus": "claude-3-opus-20240229",
|
|
363
|
+
"gpt-4o-mini": "gpt-4o-mini",
|
|
364
|
+
"gpt-4o": "gpt-4o"
|
|
365
|
+
};
|
|
366
|
+
|
|
367
|
+
return modelMap[modelName] || modelName;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* Learn from execution (async, non-blocking)
|
|
372
|
+
* Uses Reflector to extract patterns and updates skillbook
|
|
373
|
+
*/
|
|
374
|
+
async _learnFromExecution(context, successful) {
|
|
375
|
+
try {
|
|
376
|
+
// Use Reflector to extract sophisticated patterns
|
|
377
|
+
const patterns = Reflector.reflect(context, successful);
|
|
378
|
+
|
|
379
|
+
if (patterns.length === 0) {
|
|
380
|
+
return; // Nothing to learn
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// Load skillbook for this agent type
|
|
384
|
+
const skillbook = await Skillbook.load(context.agentName);
|
|
385
|
+
|
|
386
|
+
// Add each learned pattern
|
|
387
|
+
for (const pattern of patterns) {
|
|
388
|
+
skillbook.addSkill(pattern);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// Save skillbook (persists learning)
|
|
392
|
+
await skillbook.save();
|
|
393
|
+
|
|
394
|
+
logger.info({
|
|
395
|
+
agentType: context.agentName,
|
|
396
|
+
patternsLearned: patterns.length,
|
|
397
|
+
totalSkills: skillbook.skills.size,
|
|
398
|
+
successful
|
|
399
|
+
}, "Agent learned from execution");
|
|
400
|
+
|
|
401
|
+
} catch (error) {
|
|
402
|
+
// Don't throw - learning failures shouldn't break execution
|
|
403
|
+
logger.warn({
|
|
404
|
+
agentType: context.agentName,
|
|
405
|
+
error: error.message
|
|
406
|
+
}, "Learning failed");
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
module.exports = SubagentExecutor;
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
const logger = require("../logger");
|
|
2
|
+
const config = require("../config");
|
|
3
|
+
const AgentDefinitionLoader = require("./definitions/loader");
|
|
4
|
+
const ParallelCoordinator = require("./parallel-coordinator");
|
|
5
|
+
const agentStore = require("./store");
|
|
6
|
+
|
|
7
|
+
const definitionLoader = new AgentDefinitionLoader();
|
|
8
|
+
const coordinator = new ParallelCoordinator(config.agents?.maxConcurrent || 10);
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Spawn and execute subagent(s)
|
|
12
|
+
* @param {string|Array} agentType - Agent type(s) to spawn
|
|
13
|
+
* @param {string|Array} prompt - Task prompt(s)
|
|
14
|
+
* @param {Object} options - sessionId, mainContext, etc.
|
|
15
|
+
* @returns {Promise<Object>} - Execution result(s)
|
|
16
|
+
*/
|
|
17
|
+
async function spawnAgent(agentType, prompt, options = {}) {
|
|
18
|
+
if (!config.agents?.enabled) {
|
|
19
|
+
throw new Error("Agents disabled. Set AGENTS_ENABLED=true");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Handle parallel execution
|
|
23
|
+
if (Array.isArray(agentType)) {
|
|
24
|
+
return await spawnParallel(agentType, prompt, options);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Single agent execution
|
|
28
|
+
logger.info({
|
|
29
|
+
agentType,
|
|
30
|
+
prompt: prompt.slice(0, 100),
|
|
31
|
+
sessionId: options.sessionId
|
|
32
|
+
}, "Spawning subagent");
|
|
33
|
+
|
|
34
|
+
// Get agent definition
|
|
35
|
+
const agentDef = definitionLoader.getAgent(agentType);
|
|
36
|
+
|
|
37
|
+
if (!agentDef) {
|
|
38
|
+
throw new Error(`Unknown agent type: ${agentType}`);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Record in store
|
|
42
|
+
const executionId = agentStore.createExecution({
|
|
43
|
+
sessionId: options.sessionId,
|
|
44
|
+
agentType,
|
|
45
|
+
prompt,
|
|
46
|
+
model: agentDef.model
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
// Execute
|
|
50
|
+
const result = await coordinator.executeSingle(agentDef, prompt, options);
|
|
51
|
+
|
|
52
|
+
// Update store
|
|
53
|
+
if (result.success) {
|
|
54
|
+
agentStore.completeExecution(executionId, result.result, result.stats);
|
|
55
|
+
} else {
|
|
56
|
+
agentStore.failExecution(executionId, { message: result.error }, result.stats);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return result;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Spawn multiple agents in parallel
|
|
64
|
+
*/
|
|
65
|
+
async function spawnParallel(agentTypes, prompts, options = {}) {
|
|
66
|
+
if (!Array.isArray(prompts) || agentTypes.length !== prompts.length) {
|
|
67
|
+
throw new Error("agentTypes and prompts must be arrays of same length");
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
logger.info({
|
|
71
|
+
count: agentTypes.length,
|
|
72
|
+
sessionId: options.sessionId
|
|
73
|
+
}, "Spawning parallel subagents");
|
|
74
|
+
|
|
75
|
+
// Build tasks
|
|
76
|
+
const tasks = agentTypes.map((type, i) => {
|
|
77
|
+
const agentDef = definitionLoader.getAgent(type);
|
|
78
|
+
|
|
79
|
+
if (!agentDef) {
|
|
80
|
+
throw new Error(`Unknown agent type: ${type}`);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const executionId = agentStore.createExecution({
|
|
84
|
+
sessionId: options.sessionId,
|
|
85
|
+
agentType: type,
|
|
86
|
+
prompt: prompts[i],
|
|
87
|
+
model: agentDef.model
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
agentDef,
|
|
92
|
+
taskPrompt: prompts[i],
|
|
93
|
+
options: { ...options, executionId }
|
|
94
|
+
};
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// Execute in parallel with batching
|
|
98
|
+
const results = await coordinator.executeBatched(tasks);
|
|
99
|
+
|
|
100
|
+
// Update store for each result
|
|
101
|
+
results.forEach((result, i) => {
|
|
102
|
+
const executionId = tasks[i].options.executionId;
|
|
103
|
+
|
|
104
|
+
if (result.success) {
|
|
105
|
+
agentStore.completeExecution(executionId, result.result, result.stats);
|
|
106
|
+
} else {
|
|
107
|
+
agentStore.failExecution(executionId, { message: result.error }, result.stats);
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
return results;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Auto-select agent based on task description
|
|
116
|
+
*/
|
|
117
|
+
function autoSelectAgent(taskDescription) {
|
|
118
|
+
return definitionLoader.findAgentForTask(taskDescription);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Register custom agent programmatically
|
|
123
|
+
*/
|
|
124
|
+
function registerAgent(name, definition) {
|
|
125
|
+
definitionLoader.registerAgent(name, definition);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Get all available agents
|
|
130
|
+
*/
|
|
131
|
+
function listAgents() {
|
|
132
|
+
return definitionLoader.getAllAgents();
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Get agent execution stats
|
|
137
|
+
*/
|
|
138
|
+
function getAgentStats() {
|
|
139
|
+
return agentStore.getStats();
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Get specific execution details
|
|
144
|
+
*/
|
|
145
|
+
function getAgentExecution(executionId) {
|
|
146
|
+
return agentStore.getExecution(executionId);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
module.exports = {
|
|
150
|
+
spawnAgent,
|
|
151
|
+
spawnParallel,
|
|
152
|
+
autoSelectAgent,
|
|
153
|
+
registerAgent,
|
|
154
|
+
listAgents,
|
|
155
|
+
getAgentStats,
|
|
156
|
+
getAgentExecution
|
|
157
|
+
};
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
const logger = require("../logger");
|
|
2
|
+
const SubagentExecutor = require("./executor");
|
|
3
|
+
|
|
4
|
+
class ParallelCoordinator {
|
|
5
|
+
constructor(maxConcurrent = 10) {
|
|
6
|
+
this.maxConcurrent = maxConcurrent;
|
|
7
|
+
this.executor = new SubagentExecutor();
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Execute multiple subagents in parallel with batching
|
|
12
|
+
* @param {Array} tasks - Array of { agentDef, taskPrompt, options }
|
|
13
|
+
* @returns {Promise<Array>} - Array of results
|
|
14
|
+
*/
|
|
15
|
+
async executeBatched(tasks) {
|
|
16
|
+
if (tasks.length === 0) {
|
|
17
|
+
return [];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
logger.info({
|
|
21
|
+
totalTasks: tasks.length,
|
|
22
|
+
maxConcurrent: this.maxConcurrent
|
|
23
|
+
}, "Starting batched subagent execution");
|
|
24
|
+
|
|
25
|
+
const results = [];
|
|
26
|
+
let processed = 0;
|
|
27
|
+
|
|
28
|
+
// Process in batches
|
|
29
|
+
while (processed < tasks.length) {
|
|
30
|
+
const batch = tasks.slice(processed, processed + this.maxConcurrent);
|
|
31
|
+
|
|
32
|
+
logger.debug({
|
|
33
|
+
batchSize: batch.length,
|
|
34
|
+
processed,
|
|
35
|
+
remaining: tasks.length - processed - batch.length
|
|
36
|
+
}, "Processing subagent batch");
|
|
37
|
+
|
|
38
|
+
// Execute batch in parallel
|
|
39
|
+
const batchResults = await Promise.all(
|
|
40
|
+
batch.map(task => this.executor.execute(
|
|
41
|
+
task.agentDef,
|
|
42
|
+
task.taskPrompt,
|
|
43
|
+
task.options
|
|
44
|
+
))
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
results.push(...batchResults);
|
|
48
|
+
processed += batch.length;
|
|
49
|
+
|
|
50
|
+
logger.info({
|
|
51
|
+
completedInBatch: batch.length,
|
|
52
|
+
totalCompleted: processed,
|
|
53
|
+
totalTasks: tasks.length
|
|
54
|
+
}, "Completed subagent batch");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return results;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Execute single subagent (convenience method)
|
|
62
|
+
*/
|
|
63
|
+
async executeSingle(agentDef, taskPrompt, options = {}) {
|
|
64
|
+
return this.executor.execute(agentDef, taskPrompt, options);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
module.exports = ParallelCoordinator;
|