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,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;
|