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,321 @@
1
+ const logger = require("../logger");
2
+
3
+ /**
4
+ * Reflector - Analyzes agent executions and extracts sophisticated patterns
5
+ * Inspired by ACE framework's Reflector role
6
+ */
7
+ class Reflector {
8
+ /**
9
+ * Reflect on execution and generate skill recommendations
10
+ * @param {Object} context - Execution context
11
+ * @param {boolean} successful - Whether execution succeeded
12
+ * @returns {Array} - Array of skill objects
13
+ */
14
+ static reflect(context, successful) {
15
+ const skills = [];
16
+
17
+ // Skip trivial executions
18
+ if (context.steps < 2) {
19
+ return skills;
20
+ }
21
+
22
+ // Analyze tool usage patterns
23
+ const toolPatterns = this._analyzeToolUsage(context);
24
+ skills.push(...toolPatterns);
25
+
26
+ // Analyze execution efficiency
27
+ const efficiencyPatterns = this._analyzeEfficiency(context, successful);
28
+ skills.push(...efficiencyPatterns);
29
+
30
+ // Analyze error handling
31
+ const errorPatterns = this._analyzeErrors(context, successful);
32
+ skills.push(...errorPatterns);
33
+
34
+ // Analyze task-specific patterns
35
+ const taskPatterns = this._analyzeTaskPatterns(context, successful);
36
+ skills.push(...taskPatterns);
37
+
38
+ logger.debug({
39
+ agentType: context.agentName,
40
+ skillsExtracted: skills.length,
41
+ successful
42
+ }, "Reflection complete");
43
+
44
+ return skills;
45
+ }
46
+
47
+ /**
48
+ * Analyze tool usage patterns
49
+ */
50
+ static _analyzeToolUsage(context) {
51
+ const patterns = [];
52
+ const transcript = context.transcript || [];
53
+
54
+ // Extract tool sequence
55
+ const toolSequence = transcript
56
+ .filter(entry => entry.type === "tool_call" && !entry.error)
57
+ .map(entry => entry.toolName);
58
+
59
+ if (toolSequence.length === 0) {
60
+ return patterns;
61
+ }
62
+
63
+ // Pattern 1: Common tool combinations
64
+ if (toolSequence.length >= 2) {
65
+ const uniqueTools = [...new Set(toolSequence)];
66
+ const taskType = this._inferTaskType(context.taskPrompt);
67
+
68
+ if (taskType) {
69
+ patterns.push({
70
+ pattern: `${taskType} requires ${uniqueTools.length} tools`,
71
+ action: `Use: ${uniqueTools.join(" → ")}`,
72
+ reasoning: `Observed successful sequence for ${taskType.toLowerCase()}`,
73
+ tools: uniqueTools,
74
+ confidence: 0.6
75
+ });
76
+ }
77
+ }
78
+
79
+ // Pattern 2: Tool repetition (indicates iteration/refinement)
80
+ const toolCounts = {};
81
+ toolSequence.forEach(tool => {
82
+ toolCounts[tool] = (toolCounts[tool] || 0) + 1;
83
+ });
84
+
85
+ const repeatedTools = Object.entries(toolCounts)
86
+ .filter(([_, count]) => count > 1)
87
+ .map(([tool, _]) => tool);
88
+
89
+ if (repeatedTools.length > 0) {
90
+ patterns.push({
91
+ pattern: "Iterative refinement needed",
92
+ action: `Multiple calls to: ${repeatedTools.join(", ")}`,
93
+ reasoning: "Task required iterative approach",
94
+ tools: repeatedTools,
95
+ confidence: 0.5
96
+ });
97
+ }
98
+
99
+ // Pattern 3: First tool used (often indicates primary approach)
100
+ if (toolSequence.length > 0) {
101
+ const firstTool = toolSequence[0];
102
+ const taskType = this._inferTaskType(context.taskPrompt);
103
+
104
+ if (taskType) {
105
+ patterns.push({
106
+ pattern: `Start ${taskType} with exploration`,
107
+ action: `Begin with ${firstTool}`,
108
+ reasoning: `${firstTool} is effective starting point`,
109
+ tools: [firstTool],
110
+ confidence: 0.55
111
+ });
112
+ }
113
+ }
114
+
115
+ return patterns;
116
+ }
117
+
118
+ /**
119
+ * Analyze execution efficiency
120
+ */
121
+ static _analyzeEfficiency(context, successful) {
122
+ const patterns = [];
123
+
124
+ if (!successful) {
125
+ return patterns; // Only analyze successful executions for efficiency
126
+ }
127
+
128
+ const efficiency = context.steps / context.maxSteps;
129
+
130
+ // Pattern 1: Highly efficient (< 50% of max steps)
131
+ if (efficiency < 0.5) {
132
+ const toolsUsed = this._extractUniqueTools(context);
133
+
134
+ patterns.push({
135
+ pattern: "Efficient execution pattern",
136
+ action: `Quick resolution in ${context.steps} steps using ${toolsUsed.join(", ")}`,
137
+ reasoning: `Completed efficiently (${Math.round(efficiency * 100)}% of max steps)`,
138
+ tools: toolsUsed,
139
+ confidence: 0.75
140
+ });
141
+ }
142
+
143
+ // Pattern 2: Near max steps (> 80% of max)
144
+ if (efficiency > 0.8) {
145
+ patterns.push({
146
+ pattern: "Complex task requiring many steps",
147
+ action: "Consider breaking down or optimizing approach",
148
+ reasoning: `Used ${context.steps}/${context.maxSteps} steps - may need optimization`,
149
+ tools: this._extractUniqueTools(context),
150
+ confidence: 0.4
151
+ });
152
+ }
153
+
154
+ // Pattern 3: Token usage efficiency
155
+ const tokensPerStep = (context.inputTokens + context.outputTokens) / context.steps;
156
+ if (tokensPerStep < 1000) {
157
+ patterns.push({
158
+ pattern: "Token-efficient approach",
159
+ action: "Concise tool usage and responses",
160
+ reasoning: `Low token usage per step (~${Math.round(tokensPerStep)} tokens)`,
161
+ tools: [],
162
+ confidence: 0.6
163
+ });
164
+ }
165
+
166
+ return patterns;
167
+ }
168
+
169
+ /**
170
+ * Analyze error handling patterns
171
+ */
172
+ static _analyzeErrors(context, successful) {
173
+ const patterns = [];
174
+ const transcript = context.transcript || [];
175
+
176
+ const errorEntries = transcript.filter(entry =>
177
+ entry.type === "tool_call" && entry.error
178
+ );
179
+
180
+ if (errorEntries.length === 0) {
181
+ return patterns;
182
+ }
183
+
184
+ // Pattern 1: Recovered from errors
185
+ if (successful) {
186
+ const failedTools = errorEntries.map(e => e.toolName);
187
+ const recoveryTools = transcript
188
+ .slice(errorEntries[errorEntries.length - 1].timestamp)
189
+ .filter(e => e.type === "tool_call" && !e.error)
190
+ .map(e => e.toolName);
191
+
192
+ patterns.push({
193
+ pattern: "Error recovery strategy",
194
+ action: `After ${failedTools[0]} fails, try ${recoveryTools[0] || "alternative approach"}`,
195
+ reasoning: `Successfully recovered from ${errorEntries.length} error(s)`,
196
+ tools: [...new Set([...failedTools, ...recoveryTools])],
197
+ confidence: 0.65
198
+ });
199
+ }
200
+
201
+ // Pattern 2: Failed due to errors
202
+ if (!successful) {
203
+ const failedTools = [...new Set(errorEntries.map(e => e.toolName))];
204
+
205
+ patterns.push({
206
+ pattern: `Avoid ${failedTools.join(", ")} for this task type`,
207
+ action: "Use alternative tools",
208
+ reasoning: `These tools failed for ${this._inferTaskType(context.taskPrompt)}`,
209
+ tools: failedTools,
210
+ confidence: 0.3
211
+ });
212
+ }
213
+
214
+ return patterns;
215
+ }
216
+
217
+ /**
218
+ * Analyze task-specific patterns
219
+ */
220
+ static _analyzeTaskPatterns(context, successful) {
221
+ const patterns = [];
222
+
223
+ if (!successful) {
224
+ return patterns;
225
+ }
226
+
227
+ const taskType = this._inferTaskType(context.taskPrompt);
228
+ if (!taskType) {
229
+ return patterns;
230
+ }
231
+
232
+ const tools = this._extractUniqueTools(context);
233
+ const steps = context.steps;
234
+
235
+ // Build comprehensive task-specific pattern
236
+ patterns.push({
237
+ pattern: `${taskType} methodology`,
238
+ action: `Use ${tools.length} tools in sequence: ${tools.slice(0, 3).join(" → ")}${tools.length > 3 ? "..." : ""}`,
239
+ reasoning: `Proven approach for ${taskType.toLowerCase()} (${steps} steps, ${successful ? "success" : "failed"})`,
240
+ tools: tools,
241
+ confidence: this._calculatePatternConfidence(context, successful)
242
+ });
243
+
244
+ return patterns;
245
+ }
246
+
247
+ /**
248
+ * Infer task type from prompt
249
+ */
250
+ static _inferTaskType(prompt) {
251
+ const lower = prompt.toLowerCase();
252
+
253
+ const taskTypes = [
254
+ { keywords: ["find", "search", "locate", "where"], type: "Search task" },
255
+ { keywords: ["list", "show", "display", "enumerate"], type: "Listing task" },
256
+ { keywords: ["explain", "understand", "analyze", "examine"], type: "Analysis task" },
257
+ { keywords: ["fix", "repair", "debug", "solve"], type: "Fix task" },
258
+ { keywords: ["test", "verify", "check", "validate"], type: "Testing task" },
259
+ { keywords: ["refactor", "improve", "clean", "optimize"], type: "Refactoring task" },
260
+ { keywords: ["implement", "create", "add", "build"], type: "Implementation task" },
261
+ { keywords: ["document", "write", "describe"], type: "Documentation task" }
262
+ ];
263
+
264
+ for (const { keywords, type } of taskTypes) {
265
+ if (keywords.some(keyword => lower.includes(keyword))) {
266
+ return type;
267
+ }
268
+ }
269
+
270
+ return null;
271
+ }
272
+
273
+ /**
274
+ * Extract unique tools from context
275
+ */
276
+ static _extractUniqueTools(context) {
277
+ const transcript = context.transcript || [];
278
+ const toolsUsed = transcript
279
+ .filter(entry => entry.type === "tool_call" && !entry.error)
280
+ .map(entry => entry.toolName);
281
+
282
+ return [...new Set(toolsUsed)];
283
+ }
284
+
285
+ /**
286
+ * Calculate pattern confidence based on execution quality
287
+ */
288
+ static _calculatePatternConfidence(context, successful) {
289
+ if (!successful) {
290
+ return 0.25;
291
+ }
292
+
293
+ let confidence = 0.5;
294
+
295
+ // Boost for efficient execution
296
+ const efficiency = context.steps / context.maxSteps;
297
+ if (efficiency < 0.5) {
298
+ confidence += 0.2;
299
+ }
300
+
301
+ // Boost for no errors
302
+ const errorCount = (context.transcript || [])
303
+ .filter(e => e.type === "tool_call" && e.error).length;
304
+
305
+ if (errorCount === 0) {
306
+ confidence += 0.15;
307
+ } else {
308
+ confidence -= (errorCount * 0.05);
309
+ }
310
+
311
+ // Boost for comprehensive tool usage (2-5 tools)
312
+ const toolCount = this._extractUniqueTools(context).length;
313
+ if (toolCount >= 2 && toolCount <= 5) {
314
+ confidence += 0.1;
315
+ }
316
+
317
+ return Math.max(0.2, Math.min(0.9, confidence));
318
+ }
319
+ }
320
+
321
+ module.exports = Reflector;
@@ -0,0 +1,331 @@
1
+ const logger = require("../logger");
2
+ const path = require("path");
3
+ const fs = require("fs").promises;
4
+
5
+ /**
6
+ * Skillbook - Persistent knowledge store for agent learning
7
+ * Each agent type has its own skillbook that evolves with experience
8
+ */
9
+ class Skillbook {
10
+ constructor(agentType) {
11
+ this.agentType = agentType;
12
+ this.skills = new Map(); // pattern → skill object
13
+ this.loaded = false;
14
+ }
15
+
16
+ /**
17
+ * Add or update a skill
18
+ * Uses incremental merging - doesn't replace existing knowledge
19
+ */
20
+ addSkill(skill) {
21
+ if (!skill.pattern || !skill.action) {
22
+ logger.warn({ skill }, "Invalid skill format, skipping");
23
+ return false;
24
+ }
25
+
26
+ const key = this._normalizePattern(skill.pattern);
27
+ const existing = this.skills.get(key);
28
+
29
+ if (existing) {
30
+ // Merge with existing skill
31
+ existing.useCount++;
32
+ existing.lastUsed = Date.now();
33
+
34
+ // Update confidence (weighted average)
35
+ const newConfidence = skill.confidence || 0.5;
36
+ existing.confidence = (existing.confidence * 0.7) + (newConfidence * 0.3);
37
+
38
+ // Update action if new one has higher confidence
39
+ if (newConfidence > existing.confidence) {
40
+ existing.action = skill.action;
41
+ existing.reasoning = skill.reasoning || existing.reasoning;
42
+ }
43
+
44
+ logger.debug({
45
+ agentType: this.agentType,
46
+ pattern: skill.pattern,
47
+ confidence: existing.confidence
48
+ }, "Updated existing skill");
49
+ } else {
50
+ // Add new skill
51
+ this.skills.set(key, {
52
+ pattern: skill.pattern,
53
+ action: skill.action,
54
+ reasoning: skill.reasoning || "",
55
+ tools: skill.tools || [],
56
+ confidence: skill.confidence || 0.5,
57
+ useCount: 1,
58
+ createdAt: Date.now(),
59
+ lastUsed: Date.now()
60
+ });
61
+
62
+ logger.info({
63
+ agentType: this.agentType,
64
+ pattern: skill.pattern,
65
+ totalSkills: this.skills.size
66
+ }, "Added new skill");
67
+ }
68
+
69
+ return true;
70
+ }
71
+
72
+ /**
73
+ * Get top N skills for context injection
74
+ * Sorted by: confidence * useCount * recency
75
+ */
76
+ getTopSkills(n = 5) {
77
+ if (this.skills.size === 0) {
78
+ return [];
79
+ }
80
+
81
+ const now = Date.now();
82
+ const maxAge = 30 * 24 * 60 * 60 * 1000; // 30 days
83
+
84
+ return Array.from(this.skills.values())
85
+ .map(skill => {
86
+ // Recency factor (skills used recently are more valuable)
87
+ const age = now - skill.lastUsed;
88
+ const recencyFactor = Math.max(0.1, 1 - (age / maxAge));
89
+
90
+ // Combined score
91
+ const score = skill.confidence * skill.useCount * recencyFactor;
92
+
93
+ return { ...skill, score };
94
+ })
95
+ .sort((a, b) => b.score - a.score)
96
+ .slice(0, n)
97
+ .map(skill => ({
98
+ pattern: skill.pattern,
99
+ action: skill.action,
100
+ reasoning: skill.reasoning,
101
+ confidence: skill.confidence
102
+ }));
103
+ }
104
+
105
+ /**
106
+ * Format skills for prompt injection
107
+ */
108
+ formatForPrompt(skills = null) {
109
+ const topSkills = skills || this.getTopSkills(5);
110
+
111
+ if (topSkills.length === 0) {
112
+ return "";
113
+ }
114
+
115
+ const formatted = topSkills.map((skill, i) => {
116
+ const confidenceBar = "█".repeat(Math.round(skill.confidence * 5));
117
+ return `${i + 1}. ${skill.pattern}
118
+ → ${skill.action}
119
+ ${skill.reasoning ? ` Why: ${skill.reasoning}` : ''}
120
+ Confidence: ${confidenceBar} ${Math.round(skill.confidence * 100)}%`;
121
+ }).join("\n\n");
122
+
123
+ return `
124
+ ## Previously Learned Skills
125
+
126
+ You've successfully used these patterns before. Consider applying them:
127
+
128
+ ${formatted}
129
+
130
+ Apply these learnings when relevant, but don't force them if the situation differs.
131
+ `;
132
+ }
133
+
134
+ /**
135
+ * Record when a skill is used
136
+ */
137
+ recordUsage(pattern, successful = true) {
138
+ const key = this._normalizePattern(pattern);
139
+ const skill = this.skills.get(key);
140
+
141
+ if (skill) {
142
+ skill.useCount++;
143
+ skill.lastUsed = Date.now();
144
+
145
+ // Adjust confidence based on success
146
+ if (successful) {
147
+ skill.confidence = Math.min(1.0, skill.confidence + 0.05);
148
+ } else {
149
+ skill.confidence = Math.max(0.1, skill.confidence - 0.1);
150
+ }
151
+
152
+ logger.debug({
153
+ agentType: this.agentType,
154
+ pattern,
155
+ successful,
156
+ newConfidence: skill.confidence
157
+ }, "Recorded skill usage");
158
+ }
159
+ }
160
+
161
+ /**
162
+ * Prune low-quality skills
163
+ */
164
+ prune(minConfidence = 0.2, minUseCount = 3) {
165
+ let pruned = 0;
166
+
167
+ for (const [key, skill] of this.skills.entries()) {
168
+ // Remove skills that have been tried multiple times but remain low confidence
169
+ if (skill.useCount >= minUseCount && skill.confidence < minConfidence) {
170
+ this.skills.delete(key);
171
+ pruned++;
172
+ }
173
+ }
174
+
175
+ if (pruned > 0) {
176
+ logger.info({
177
+ agentType: this.agentType,
178
+ pruned,
179
+ remaining: this.skills.size
180
+ }, "Pruned low-confidence skills");
181
+ }
182
+
183
+ return pruned;
184
+ }
185
+
186
+ /**
187
+ * Get statistics
188
+ */
189
+ getStats() {
190
+ const skills = Array.from(this.skills.values());
191
+
192
+ return {
193
+ agentType: this.agentType,
194
+ totalSkills: skills.length,
195
+ averageConfidence: skills.reduce((sum, s) => sum + s.confidence, 0) / skills.length || 0,
196
+ totalUses: skills.reduce((sum, s) => sum + s.useCount, 0),
197
+ highConfidenceSkills: skills.filter(s => s.confidence >= 0.8).length
198
+ };
199
+ }
200
+
201
+ /**
202
+ * Save to disk (JSON format)
203
+ */
204
+ async save() {
205
+ const filepath = this._getFilePath();
206
+
207
+ try {
208
+ // Ensure directory exists
209
+ const dir = path.dirname(filepath);
210
+ await fs.mkdir(dir, { recursive: true });
211
+
212
+ const data = {
213
+ agentType: this.agentType,
214
+ skills: Array.from(this.skills.entries()),
215
+ savedAt: Date.now(),
216
+ version: "1.0"
217
+ };
218
+
219
+ await fs.writeFile(filepath, JSON.stringify(data, null, 2), 'utf8');
220
+
221
+ logger.debug({
222
+ agentType: this.agentType,
223
+ filepath,
224
+ skillCount: this.skills.size
225
+ }, "Saved skillbook");
226
+
227
+ return true;
228
+ } catch (error) {
229
+ logger.error({
230
+ error: error.message,
231
+ agentType: this.agentType,
232
+ filepath
233
+ }, "Failed to save skillbook");
234
+ return false;
235
+ }
236
+ }
237
+
238
+ /**
239
+ * Load from disk
240
+ */
241
+ async load() {
242
+ if (this.loaded) {
243
+ return true; // Already loaded
244
+ }
245
+
246
+ const filepath = this._getFilePath();
247
+
248
+ try {
249
+ const content = await fs.readFile(filepath, 'utf8');
250
+ const data = JSON.parse(content);
251
+
252
+ if (data.agentType !== this.agentType) {
253
+ throw new Error(`Agent type mismatch: expected ${this.agentType}, got ${data.agentType}`);
254
+ }
255
+
256
+ // Restore skills map
257
+ this.skills = new Map(data.skills);
258
+ this.loaded = true;
259
+
260
+ logger.info({
261
+ agentType: this.agentType,
262
+ skillCount: this.skills.size,
263
+ filepath
264
+ }, "Loaded skillbook");
265
+
266
+ return true;
267
+ } catch (error) {
268
+ if (error.code === 'ENOENT') {
269
+ // File doesn't exist yet - this is fine for new agents
270
+ logger.debug({
271
+ agentType: this.agentType,
272
+ filepath
273
+ }, "No existing skillbook found, starting fresh");
274
+ this.loaded = true;
275
+ return true;
276
+ }
277
+
278
+ logger.error({
279
+ error: error.message,
280
+ agentType: this.agentType,
281
+ filepath
282
+ }, "Failed to load skillbook");
283
+
284
+ this.loaded = true; // Mark as loaded to prevent retries
285
+ return false;
286
+ }
287
+ }
288
+
289
+ /**
290
+ * Clear all skills (use with caution)
291
+ */
292
+ clear() {
293
+ this.skills.clear();
294
+ logger.warn({
295
+ agentType: this.agentType
296
+ }, "Cleared skillbook");
297
+ }
298
+
299
+ /**
300
+ * Get file path for this agent's skillbook
301
+ */
302
+ _getFilePath() {
303
+ const dataDir = path.join(process.cwd(), 'data', 'skillbooks');
304
+ return path.join(dataDir, `${this.agentType.toLowerCase()}.json`);
305
+ }
306
+
307
+ /**
308
+ * Normalize pattern for consistent matching
309
+ */
310
+ _normalizePattern(pattern) {
311
+ return pattern.toLowerCase().trim().replace(/\s+/g, ' ');
312
+ }
313
+
314
+ /**
315
+ * Static method: Load skillbook for agent type
316
+ */
317
+ static async load(agentType) {
318
+ const skillbook = new Skillbook(agentType);
319
+ await skillbook.load();
320
+ return skillbook;
321
+ }
322
+
323
+ /**
324
+ * Static method: Get or create skillbook
325
+ */
326
+ static async getOrCreate(agentType) {
327
+ return Skillbook.load(agentType);
328
+ }
329
+ }
330
+
331
+ module.exports = Skillbook;