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.
Files changed (41) hide show
  1. package/CITATIONS.bib +6 -0
  2. package/DEPLOYMENT.md +1001 -0
  3. package/README.md +215 -71
  4. package/docs/index.md +55 -2
  5. package/monitor-agents.sh +31 -0
  6. package/package.json +7 -3
  7. package/src/agents/context-manager.js +220 -0
  8. package/src/agents/definitions/loader.js +563 -0
  9. package/src/agents/executor.js +412 -0
  10. package/src/agents/index.js +157 -0
  11. package/src/agents/parallel-coordinator.js +68 -0
  12. package/src/agents/reflector.js +321 -0
  13. package/src/agents/skillbook.js +331 -0
  14. package/src/agents/store.js +244 -0
  15. package/src/api/router.js +55 -0
  16. package/src/clients/databricks.js +214 -17
  17. package/src/clients/routing.js +15 -7
  18. package/src/clients/standard-tools.js +341 -0
  19. package/src/config/index.js +41 -5
  20. package/src/orchestrator/index.js +254 -37
  21. package/src/server.js +2 -0
  22. package/src/tools/agent-task.js +96 -0
  23. package/test/azure-openai-config.test.js +203 -0
  24. package/test/azure-openai-error-resilience.test.js +238 -0
  25. package/test/azure-openai-format-conversion.test.js +354 -0
  26. package/test/azure-openai-integration.test.js +281 -0
  27. package/test/azure-openai-routing.test.js +148 -0
  28. package/test/azure-openai-streaming.test.js +171 -0
  29. package/test/format-conversion.test.js +578 -0
  30. package/test/hybrid-routing-integration.test.js +18 -11
  31. package/test/openrouter-error-resilience.test.js +418 -0
  32. package/test/passthrough-mode.test.js +385 -0
  33. package/test/routing.test.js +9 -3
  34. package/test/web-tools.test.js +3 -0
  35. package/test-agents-simple.js +43 -0
  36. package/test-cli-connection.sh +33 -0
  37. package/test-learning-unit.js +126 -0
  38. package/test-learning.js +112 -0
  39. package/test-parallel-agents.sh +124 -0
  40. package/test-parallel-direct.js +155 -0
  41. 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;