@koi-language/koi 1.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/QUICKSTART.md +89 -0
- package/README.md +545 -0
- package/examples/actions-demo.koi +177 -0
- package/examples/cache-test.koi +29 -0
- package/examples/calculator.koi +61 -0
- package/examples/clear-registry.js +33 -0
- package/examples/clear-registry.koi +30 -0
- package/examples/code-introspection-test.koi +149 -0
- package/examples/counter.koi +132 -0
- package/examples/delegation-test.koi +52 -0
- package/examples/directory-import-test.koi +84 -0
- package/examples/hello-world-claude.koi +52 -0
- package/examples/hello-world.koi +52 -0
- package/examples/hello.koi +24 -0
- package/examples/mcp-example.koi +70 -0
- package/examples/multi-event-handler-test.koi +144 -0
- package/examples/new-import-test.koi +89 -0
- package/examples/pipeline.koi +162 -0
- package/examples/registry-demo.koi +184 -0
- package/examples/registry-playbook-demo.koi +162 -0
- package/examples/registry-playbook-email-compositor-2.koi +140 -0
- package/examples/registry-playbook-email-compositor.koi +140 -0
- package/examples/sentiment.koi +90 -0
- package/examples/simple.koi +48 -0
- package/examples/skill-import-test.koi +76 -0
- package/examples/skills/advanced/index.koi +95 -0
- package/examples/skills/math-operations.koi +69 -0
- package/examples/skills/string-operations.koi +56 -0
- package/examples/task-chaining-demo.koi +244 -0
- package/examples/test-await.koi +22 -0
- package/examples/test-crypto-sha256.koi +196 -0
- package/examples/test-delegation.koi +41 -0
- package/examples/test-multi-team-routing.koi +258 -0
- package/examples/test-no-handler.koi +35 -0
- package/examples/test-npm-import.koi +67 -0
- package/examples/test-parse.koi +10 -0
- package/examples/test-peers-with-team.koi +59 -0
- package/examples/test-permissions-fail.koi +20 -0
- package/examples/test-permissions.koi +36 -0
- package/examples/test-simple-registry.koi +31 -0
- package/examples/test-typescript-import.koi +64 -0
- package/examples/test-uses-team-syntax.koi +25 -0
- package/examples/test-uses-team.koi +31 -0
- package/examples/utils/calculator.test.ts +144 -0
- package/examples/utils/calculator.ts +56 -0
- package/examples/utils/math-helpers.js +50 -0
- package/examples/utils/math-helpers.ts +55 -0
- package/examples/web-delegation-demo.koi +165 -0
- package/package.json +78 -0
- package/src/cli/koi.js +793 -0
- package/src/compiler/build-optimizer.js +447 -0
- package/src/compiler/cache-manager.js +274 -0
- package/src/compiler/import-resolver.js +369 -0
- package/src/compiler/parser.js +7542 -0
- package/src/compiler/transpiler.js +1105 -0
- package/src/compiler/typescript-transpiler.js +148 -0
- package/src/grammar/koi.pegjs +767 -0
- package/src/runtime/action-registry.js +172 -0
- package/src/runtime/actions/call-skill.js +45 -0
- package/src/runtime/actions/format.js +115 -0
- package/src/runtime/actions/print.js +42 -0
- package/src/runtime/actions/registry-delete.js +37 -0
- package/src/runtime/actions/registry-get.js +37 -0
- package/src/runtime/actions/registry-keys.js +33 -0
- package/src/runtime/actions/registry-search.js +34 -0
- package/src/runtime/actions/registry-set.js +50 -0
- package/src/runtime/actions/return.js +31 -0
- package/src/runtime/actions/send-message.js +58 -0
- package/src/runtime/actions/update-state.js +36 -0
- package/src/runtime/agent.js +1368 -0
- package/src/runtime/cli-logger.js +205 -0
- package/src/runtime/incremental-json-parser.js +201 -0
- package/src/runtime/index.js +33 -0
- package/src/runtime/llm-provider.js +1372 -0
- package/src/runtime/mcp-client.js +1171 -0
- package/src/runtime/planner.js +273 -0
- package/src/runtime/registry-backends/keyv-sqlite.js +215 -0
- package/src/runtime/registry-backends/local.js +260 -0
- package/src/runtime/registry.js +162 -0
- package/src/runtime/role.js +14 -0
- package/src/runtime/router.js +395 -0
- package/src/runtime/runtime.js +113 -0
- package/src/runtime/skill-selector.js +173 -0
- package/src/runtime/skill.js +25 -0
- package/src/runtime/team.js +162 -0
|
@@ -0,0 +1,395 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent Router with Intelligent Semantic Matching
|
|
3
|
+
*
|
|
4
|
+
* Uses a hybrid approach:
|
|
5
|
+
* 1. Fast embedding-based similarity search for initial filtering
|
|
6
|
+
* 2. LLM-based disambiguation when needed
|
|
7
|
+
*
|
|
8
|
+
* This allows agents to automatically discover and route tasks to the
|
|
9
|
+
* appropriate agent based on semantic understanding of capabilities.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { LLMProvider } from './llm-provider.js';
|
|
13
|
+
import { cliLogger } from './cli-logger.js';
|
|
14
|
+
|
|
15
|
+
export class AgentRouter {
|
|
16
|
+
constructor(config = {}) {
|
|
17
|
+
this.agents = new Map(); // Map<agentName, agent>
|
|
18
|
+
this.affordanceEmbeddings = []; // Array of { agent, event, description, embedding, confidence }
|
|
19
|
+
this.embeddingProvider = null;
|
|
20
|
+
this.llmProvider = null;
|
|
21
|
+
|
|
22
|
+
// Configuration
|
|
23
|
+
this.similarityThreshold = config.similarityThreshold || 0.4; // Balanced threshold for semantic matching
|
|
24
|
+
this.highConfidenceThreshold = config.highConfidenceThreshold || 0.85;
|
|
25
|
+
this.useLLMDisambiguation = config.useLLMDisambiguation !== false;
|
|
26
|
+
this.cacheEmbeddings = config.cacheEmbeddings !== false;
|
|
27
|
+
this.verbose = config.verbose || false;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Register an agent and extract its affordances
|
|
32
|
+
* @param agent - The agent to register
|
|
33
|
+
* @param cachedAffordances - Optional pre-computed affordances from build cache
|
|
34
|
+
*/
|
|
35
|
+
async register(agent, cachedAffordances = null) {
|
|
36
|
+
if (!agent || !agent.name) {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
this.agents.set(agent.name, agent);
|
|
41
|
+
|
|
42
|
+
// Use cached affordances if available
|
|
43
|
+
if (cachedAffordances) {
|
|
44
|
+
for (const [eventName, aff] of Object.entries(cachedAffordances)) {
|
|
45
|
+
if (!aff.embedding) {
|
|
46
|
+
// Fallback: generate at runtime if cache is incomplete
|
|
47
|
+
if (aff.description && aff.description.trim() !== '') {
|
|
48
|
+
aff.embedding = await this.getEmbedding(aff.description);
|
|
49
|
+
} else {
|
|
50
|
+
console.warn(`⚠️ [Router] Skipping ${agent.name}.${eventName} - empty description`);
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
this.affordanceEmbeddings.push({
|
|
56
|
+
agent: agent,
|
|
57
|
+
event: eventName,
|
|
58
|
+
description: aff.description,
|
|
59
|
+
embedding: aff.embedding,
|
|
60
|
+
confidence: aff.confidence,
|
|
61
|
+
metadata: { hasPlaybook: aff.hasPlaybook }
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// No cache: extract and generate affordances at runtime
|
|
69
|
+
const affordances = this.extractAffordances(agent);
|
|
70
|
+
|
|
71
|
+
// Generate embeddings for each affordance
|
|
72
|
+
for (const aff of affordances) {
|
|
73
|
+
if (!aff.description || aff.description.trim() === '') {
|
|
74
|
+
console.warn(`⚠️ [Router] Skipping ${agent.name}.${aff.event} - empty description`);
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const embedding = await this.getEmbedding(aff.description);
|
|
79
|
+
|
|
80
|
+
this.affordanceEmbeddings.push({
|
|
81
|
+
agent: agent,
|
|
82
|
+
event: aff.event,
|
|
83
|
+
description: aff.description,
|
|
84
|
+
embedding: embedding,
|
|
85
|
+
confidence: aff.confidence,
|
|
86
|
+
metadata: aff.metadata
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Extract affordances from an agent by analyzing its handlers and playbooks
|
|
93
|
+
*/
|
|
94
|
+
extractAffordances(agent) {
|
|
95
|
+
const affordances = [];
|
|
96
|
+
|
|
97
|
+
if (!agent.handlers) {
|
|
98
|
+
return affordances;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
for (const [eventName, handler] of Object.entries(agent.handlers)) {
|
|
102
|
+
// Try to infer description from playbook
|
|
103
|
+
const playbook = agent.playbooks?.[eventName];
|
|
104
|
+
|
|
105
|
+
let description;
|
|
106
|
+
let confidence;
|
|
107
|
+
|
|
108
|
+
if (playbook) {
|
|
109
|
+
description = this.inferIntentFromPlaybook(playbook, eventName);
|
|
110
|
+
confidence = 0.9; // High confidence when we have playbook
|
|
111
|
+
} else {
|
|
112
|
+
description = `Handle ${eventName} event`;
|
|
113
|
+
confidence = 0.5; // Lower confidence without playbook
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
affordances.push({
|
|
117
|
+
event: eventName,
|
|
118
|
+
description: description,
|
|
119
|
+
confidence: confidence,
|
|
120
|
+
metadata: {
|
|
121
|
+
hasPlaybook: !!playbook,
|
|
122
|
+
role: agent.role?.name
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return affordances;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Infer the intent/capability from a playbook text
|
|
132
|
+
*/
|
|
133
|
+
inferIntentFromPlaybook(playbook, eventName) {
|
|
134
|
+
// Remove template literals and get clean text
|
|
135
|
+
const cleanText = playbook
|
|
136
|
+
.replace(/\$\{[^}]+\}/g, '') // Remove ${...}
|
|
137
|
+
.split('\n')
|
|
138
|
+
.map(line => line.trim())
|
|
139
|
+
.filter(line => line.length > 0 && !line.startsWith('//'))
|
|
140
|
+
.slice(0, 3) // Take first 3 meaningful lines
|
|
141
|
+
.join(' ');
|
|
142
|
+
|
|
143
|
+
// If we got something meaningful, use it
|
|
144
|
+
if (cleanText.length > 10 && cleanText.length < 200) {
|
|
145
|
+
return cleanText;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Fallback to event name processing
|
|
149
|
+
return this.humanizeEventName(eventName);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Convert camelCase/snake_case event names to readable descriptions
|
|
154
|
+
*/
|
|
155
|
+
humanizeEventName(eventName) {
|
|
156
|
+
return eventName
|
|
157
|
+
.replace(/([A-Z])/g, ' $1') // camelCase
|
|
158
|
+
.replace(/_/g, ' ') // snake_case
|
|
159
|
+
.toLowerCase()
|
|
160
|
+
.trim();
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Get embedding for text (with caching)
|
|
165
|
+
*/
|
|
166
|
+
async getEmbedding(text) {
|
|
167
|
+
if (!this.embeddingProvider) {
|
|
168
|
+
this.embeddingProvider = new LLMProvider({
|
|
169
|
+
provider: 'openai',
|
|
170
|
+
model: 'text-embedding-3-small'
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return await this.embeddingProvider.getEmbedding(text);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Calculate cosine similarity between two vectors
|
|
179
|
+
*/
|
|
180
|
+
cosineSimilarity(vecA, vecB) {
|
|
181
|
+
if (!vecA || !vecB || vecA.length !== vecB.length) {
|
|
182
|
+
return 0;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const dotProduct = vecA.reduce((sum, a, i) => sum + a * vecB[i], 0);
|
|
186
|
+
const magnitudeA = Math.sqrt(vecA.reduce((sum, a) => sum + a * a, 0));
|
|
187
|
+
const magnitudeB = Math.sqrt(vecB.reduce((sum, b) => sum + b * b, 0));
|
|
188
|
+
|
|
189
|
+
if (magnitudeA === 0 || magnitudeB === 0) {
|
|
190
|
+
return 0;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return dotProduct / (magnitudeA * magnitudeB);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Find matching agents using hybrid approach (embeddings + optional LLM)
|
|
198
|
+
*/
|
|
199
|
+
async findMatches(intent, topK = 3) {
|
|
200
|
+
if (this.affordanceEmbeddings.length === 0) {
|
|
201
|
+
return [];
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Validate intent
|
|
205
|
+
if (!intent || typeof intent !== 'string' || intent.trim() === '') {
|
|
206
|
+
return [];
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Phase 1: Embedding-based similarity search
|
|
210
|
+
const intentEmbedding = await this.getEmbedding(intent);
|
|
211
|
+
|
|
212
|
+
const similarities = this.affordanceEmbeddings.map(aff => ({
|
|
213
|
+
...aff,
|
|
214
|
+
similarity: this.cosineSimilarity(intentEmbedding, aff.embedding)
|
|
215
|
+
}));
|
|
216
|
+
|
|
217
|
+
// Sort by similarity descending
|
|
218
|
+
similarities.sort((a, b) => b.similarity - a.similarity);
|
|
219
|
+
|
|
220
|
+
// Filter by threshold
|
|
221
|
+
const candidates = similarities
|
|
222
|
+
.filter(s => s.similarity >= this.similarityThreshold)
|
|
223
|
+
.slice(0, Math.max(topK, 5)); // Get at least top 5 for LLM phase
|
|
224
|
+
|
|
225
|
+
if (candidates.length === 0) {
|
|
226
|
+
return [];
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Phase 2: High confidence early exit
|
|
230
|
+
if (candidates[0].similarity >= this.highConfidenceThreshold) {
|
|
231
|
+
return [candidates[0]];
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Phase 3: LLM disambiguation if multiple similar candidates
|
|
235
|
+
if (this.useLLMDisambiguation && candidates.length > 1) {
|
|
236
|
+
const topCandidates = candidates.slice(0, topK);
|
|
237
|
+
|
|
238
|
+
// Check if top candidates are close in similarity (ambiguous)
|
|
239
|
+
const similarityRange = topCandidates[0].similarity - topCandidates[topCandidates.length - 1].similarity;
|
|
240
|
+
|
|
241
|
+
if (similarityRange < 0.15) { // Within 15% similarity - ambiguous
|
|
242
|
+
return await this.disambiguateWithLLM(intent, topCandidates);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Return top candidate
|
|
247
|
+
return [candidates[0]];
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Use LLM to disambiguate between similar candidates
|
|
252
|
+
*/
|
|
253
|
+
async disambiguateWithLLM(intent, candidates) {
|
|
254
|
+
if (!this.llmProvider) {
|
|
255
|
+
this.llmProvider = new LLMProvider({
|
|
256
|
+
provider: 'openai',
|
|
257
|
+
model: 'gpt-4o-mini',
|
|
258
|
+
temperature: 0.1,
|
|
259
|
+
max_tokens: 300
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const candidateDescriptions = candidates.map((c, idx) => ({
|
|
264
|
+
id: idx,
|
|
265
|
+
agent: c.agent.name,
|
|
266
|
+
event: c.event,
|
|
267
|
+
description: c.description,
|
|
268
|
+
similarity: (c.similarity * 100).toFixed(1) + '%',
|
|
269
|
+
role: c.metadata?.role
|
|
270
|
+
}));
|
|
271
|
+
|
|
272
|
+
const prompt = `You are a task router. Select the BEST agent to handle this specific task.
|
|
273
|
+
|
|
274
|
+
Task intent: "${intent}"
|
|
275
|
+
|
|
276
|
+
Available agents (pre-filtered by semantic similarity):
|
|
277
|
+
${JSON.stringify(candidateDescriptions, null, 2)}
|
|
278
|
+
|
|
279
|
+
Which agent is the BEST match? Consider:
|
|
280
|
+
- Semantic meaning and nuances of the task
|
|
281
|
+
- Agent descriptions and capabilities
|
|
282
|
+
- Task-specific requirements
|
|
283
|
+
|
|
284
|
+
Return ONLY valid JSON (no markdown):
|
|
285
|
+
{
|
|
286
|
+
"best_match_id": <id from 0 to ${candidates.length - 1}>,
|
|
287
|
+
"confidence": <number 0-1>,
|
|
288
|
+
"reasoning": "brief 1-sentence explanation"
|
|
289
|
+
}`;
|
|
290
|
+
|
|
291
|
+
try {
|
|
292
|
+
const result = await this.llmProvider.executePlaybook(prompt, {});
|
|
293
|
+
|
|
294
|
+
if (typeof result.best_match_id === 'number' && result.best_match_id >= 0 && result.best_match_id < candidates.length) {
|
|
295
|
+
const selected = candidates[result.best_match_id];
|
|
296
|
+
|
|
297
|
+
return [{
|
|
298
|
+
...selected,
|
|
299
|
+
similarity: result.confidence,
|
|
300
|
+
reasoning: result.reasoning,
|
|
301
|
+
llmDisambiguated: true
|
|
302
|
+
}];
|
|
303
|
+
} else {
|
|
304
|
+
return [candidates[0]];
|
|
305
|
+
}
|
|
306
|
+
} catch (error) {
|
|
307
|
+
return [candidates[0]];
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Check if any agent can handle this intent
|
|
313
|
+
*/
|
|
314
|
+
async canHandle(intent) {
|
|
315
|
+
const matches = await this.findMatches(intent, 1);
|
|
316
|
+
return matches.length > 0;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Get list of agents that can handle this intent
|
|
321
|
+
*/
|
|
322
|
+
async whoCanHandle(intent, topK = 3) {
|
|
323
|
+
const matches = await this.findMatches(intent, topK);
|
|
324
|
+
return matches.map(m => ({
|
|
325
|
+
agent: m.agent.name,
|
|
326
|
+
event: m.event,
|
|
327
|
+
description: m.description,
|
|
328
|
+
similarity: m.similarity,
|
|
329
|
+
confidence: m.confidence,
|
|
330
|
+
reasoning: m.reasoning
|
|
331
|
+
}));
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Route a task to the best matching agent
|
|
336
|
+
*/
|
|
337
|
+
async route(task) {
|
|
338
|
+
const intent = task.intent || task.description || task.type;
|
|
339
|
+
|
|
340
|
+
if (!intent) {
|
|
341
|
+
throw new Error('[Router] Task must have an intent, description, or type');
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
const matches = await this.findMatches(intent, 1);
|
|
345
|
+
|
|
346
|
+
if (matches.length === 0) {
|
|
347
|
+
throw new Error(`[Router] No agent can handle: "${intent}"`);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
const best = matches[0];
|
|
351
|
+
|
|
352
|
+
// Execute the task on the selected agent
|
|
353
|
+
return await best.agent.handle(best.event, task.data || {});
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Get summary of registered agents and their capabilities
|
|
358
|
+
*/
|
|
359
|
+
getSummary() {
|
|
360
|
+
const agentSummaries = [];
|
|
361
|
+
|
|
362
|
+
for (const [name, agent] of this.agents) {
|
|
363
|
+
const affordances = this.affordanceEmbeddings
|
|
364
|
+
.filter(aff => aff.agent === agent)
|
|
365
|
+
.map(aff => ({
|
|
366
|
+
event: aff.event,
|
|
367
|
+
description: aff.description,
|
|
368
|
+
confidence: aff.confidence
|
|
369
|
+
}));
|
|
370
|
+
|
|
371
|
+
agentSummaries.push({
|
|
372
|
+
name: name,
|
|
373
|
+
role: agent.role?.name,
|
|
374
|
+
affordances: affordances
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
return {
|
|
379
|
+
totalAgents: this.agents.size,
|
|
380
|
+
totalAffordances: this.affordanceEmbeddings.length,
|
|
381
|
+
agents: agentSummaries
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Clear all registered agents (useful for testing)
|
|
387
|
+
*/
|
|
388
|
+
clear() {
|
|
389
|
+
this.agents.clear();
|
|
390
|
+
this.affordanceEmbeddings = [];
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// Singleton instance for global use
|
|
395
|
+
export const agentRouter = new AgentRouter();
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { agentRouter } from './router.js';
|
|
2
|
+
import { cliLogger } from './cli-logger.js';
|
|
3
|
+
|
|
4
|
+
export class Runtime {
|
|
5
|
+
static async send(config) {
|
|
6
|
+
const { base, filters = [], args = {}, timeout = 30000 } = config;
|
|
7
|
+
|
|
8
|
+
// Validate that base (team/peers) is not null
|
|
9
|
+
if (!base || base === null || base === undefined) {
|
|
10
|
+
const eventName = filters.find(f => f.type === 'event')?.name || 'unknown event';
|
|
11
|
+
throw new Error(`NO_AGENT_HANDLER:${eventName}::no-team`);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// Apply timeout
|
|
15
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
16
|
+
setTimeout(() => reject(new Error(`Timeout after ${timeout}ms`)), timeout);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
// Build query from filters
|
|
21
|
+
let query = base;
|
|
22
|
+
|
|
23
|
+
for (const filter of filters) {
|
|
24
|
+
if (filter.type === 'event') {
|
|
25
|
+
query = query.event(filter.name);
|
|
26
|
+
} else if (filter.type === 'role') {
|
|
27
|
+
query = query.role(filter.role);
|
|
28
|
+
} else if (filter.type === 'select') {
|
|
29
|
+
query = filter.mode === 'any' ? query.any() : query.all();
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Execute with timeout
|
|
34
|
+
const result = await Promise.race([
|
|
35
|
+
query.execute(args),
|
|
36
|
+
timeoutPromise
|
|
37
|
+
]);
|
|
38
|
+
|
|
39
|
+
cliLogger.clear();
|
|
40
|
+
return result;
|
|
41
|
+
} catch (error) {
|
|
42
|
+
// Handle NO_AGENT_HANDLER errors specially
|
|
43
|
+
if (error.message && error.message.startsWith('NO_AGENT_HANDLER:')) {
|
|
44
|
+
const parts = error.message.split(':');
|
|
45
|
+
const eventName = parts[1] || 'unknown';
|
|
46
|
+
const roleInfo = parts[2] || '';
|
|
47
|
+
const teamName = parts[3] || '';
|
|
48
|
+
|
|
49
|
+
if (teamName === 'no-team') {
|
|
50
|
+
console.error(`\n❌ No agent available to handle event "${eventName}" - no team configured\n`);
|
|
51
|
+
} else {
|
|
52
|
+
const roleMsg = roleInfo ? ` (role: ${roleInfo})` : '';
|
|
53
|
+
console.error(`\n❌ No agent available to handle event "${eventName}"${roleMsg}\n`);
|
|
54
|
+
}
|
|
55
|
+
process.exit(1);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
console.error('[Runtime] Send failed:', error.message);
|
|
59
|
+
throw error;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Create an agent and auto-register it with the router
|
|
65
|
+
*/
|
|
66
|
+
static async createAgent(AgentClass, config) {
|
|
67
|
+
const agent = new AgentClass(config);
|
|
68
|
+
|
|
69
|
+
// Auto-register with router for dynamic discovery
|
|
70
|
+
if (agent.handlers && Object.keys(agent.handlers).length > 0) {
|
|
71
|
+
await agentRouter.register(agent);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return agent;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Create a team and auto-register all member agents
|
|
79
|
+
*/
|
|
80
|
+
static async createTeam(TeamClass, name, members) {
|
|
81
|
+
const team = new TeamClass(name, members);
|
|
82
|
+
|
|
83
|
+
// Register all members with the router
|
|
84
|
+
for (const [memberName, agent] of Object.entries(members)) {
|
|
85
|
+
if (agent.handlers && Object.keys(agent.handlers).length > 0) {
|
|
86
|
+
await agentRouter.register(agent);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return team;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Register an existing agent with the router
|
|
95
|
+
*/
|
|
96
|
+
static async registerAgent(agent) {
|
|
97
|
+
if (agent.handlers && Object.keys(agent.handlers).length > 0) {
|
|
98
|
+
await agentRouter.register(agent);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Get router summary for debugging
|
|
104
|
+
*/
|
|
105
|
+
static getRouterSummary() {
|
|
106
|
+
return agentRouter.getSummary();
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
static log(message, level = 'info') {
|
|
110
|
+
const timestamp = new Date().toISOString();
|
|
111
|
+
console.log(`[${timestamp}] [${level.toUpperCase()}] ${message}`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skill Selector with Semantic Matching
|
|
3
|
+
*
|
|
4
|
+
* Uses embedding-based similarity search to intelligently select
|
|
5
|
+
* which skills are relevant for a given task, reducing the number
|
|
6
|
+
* of tools passed to the LLM and improving accuracy.
|
|
7
|
+
*
|
|
8
|
+
* Similar approach to AgentRouter but for skill selection.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { LLMProvider } from './llm-provider.js';
|
|
12
|
+
|
|
13
|
+
export class SkillSelector {
|
|
14
|
+
constructor(config = {}) {
|
|
15
|
+
this.skillAffordances = []; // Array of { skillName, description, embedding, confidence, functions }
|
|
16
|
+
this.embeddingProvider = null;
|
|
17
|
+
|
|
18
|
+
// Configuration
|
|
19
|
+
this.similarityThreshold = config.similarityThreshold || 0.35; // Lower threshold for skills
|
|
20
|
+
this.verbose = config.verbose || false;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Register a skill with its affordance and available functions
|
|
25
|
+
* @param skillName - Name of the skill
|
|
26
|
+
* @param functions - Array of function objects { name, fn, description }
|
|
27
|
+
* @param cachedAffordance - Optional pre-computed affordance from build cache
|
|
28
|
+
*/
|
|
29
|
+
async register(skillName, functions, cachedAffordance = null) {
|
|
30
|
+
if (!skillName || !functions || functions.length === 0) {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
let description, embedding, confidence;
|
|
35
|
+
|
|
36
|
+
// Use cached affordance if available
|
|
37
|
+
if (cachedAffordance) {
|
|
38
|
+
description = cachedAffordance.description;
|
|
39
|
+
embedding = cachedAffordance.embedding;
|
|
40
|
+
confidence = cachedAffordance.confidence || 0.9;
|
|
41
|
+
|
|
42
|
+
// Generate embedding at runtime if cache is incomplete
|
|
43
|
+
if (!embedding) {
|
|
44
|
+
if (description && description.trim() !== '') {
|
|
45
|
+
embedding = await this.getEmbedding(description);
|
|
46
|
+
} else {
|
|
47
|
+
console.warn(`⚠️ [SkillSelector] Skipping skill ${skillName} - empty description`);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
} else {
|
|
52
|
+
// No cache: use function descriptions as affordance
|
|
53
|
+
description = functions.map(f => f.description).join('. ');
|
|
54
|
+
if (!description || description.trim() === '') {
|
|
55
|
+
console.warn(`⚠️ [SkillSelector] Skipping skill ${skillName} - no function descriptions`);
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
embedding = await this.getEmbedding(description);
|
|
59
|
+
confidence = 0.7;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
this.skillAffordances.push({
|
|
63
|
+
skillName,
|
|
64
|
+
description,
|
|
65
|
+
embedding,
|
|
66
|
+
confidence,
|
|
67
|
+
functions
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Select relevant skills for a given task/playbook using semantic matching
|
|
73
|
+
* @param playbookContent - The playbook text describing what needs to be done
|
|
74
|
+
* @param maxSkills - Maximum number of skills to return
|
|
75
|
+
* @returns Array of function objects to pass to LLM
|
|
76
|
+
*/
|
|
77
|
+
async selectSkillsForTask(playbookContent, maxSkills = 2) {
|
|
78
|
+
if (this.skillAffordances.length === 0) {
|
|
79
|
+
return [];
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// If playbook is too short or generic, return all skills
|
|
83
|
+
if (!playbookContent || playbookContent.trim().length < 20) {
|
|
84
|
+
// Return all available functions from all skills
|
|
85
|
+
return this.skillAffordances.flatMap(skill => skill.functions);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Extract the meaningful part of the playbook (first few sentences)
|
|
89
|
+
const intent = this.extractIntent(playbookContent);
|
|
90
|
+
|
|
91
|
+
// Generate embedding for the task intent
|
|
92
|
+
const intentEmbedding = await this.getEmbedding(intent);
|
|
93
|
+
|
|
94
|
+
// Calculate similarity with each skill
|
|
95
|
+
const similarities = this.skillAffordances.map(skill => ({
|
|
96
|
+
...skill,
|
|
97
|
+
similarity: this.cosineSimilarity(intentEmbedding, skill.embedding)
|
|
98
|
+
}));
|
|
99
|
+
|
|
100
|
+
// Sort by similarity descending
|
|
101
|
+
similarities.sort((a, b) => b.similarity - a.similarity);
|
|
102
|
+
|
|
103
|
+
// Filter by threshold and take top N
|
|
104
|
+
const relevantSkills = similarities
|
|
105
|
+
.filter(s => s.similarity >= this.similarityThreshold)
|
|
106
|
+
.slice(0, maxSkills);
|
|
107
|
+
|
|
108
|
+
if (this.verbose && relevantSkills.length > 0) {
|
|
109
|
+
console.log(`[SkillSelector] Selected ${relevantSkills.length} skills for: "${intent.substring(0, 50)}..."`);
|
|
110
|
+
relevantSkills.forEach(skill => {
|
|
111
|
+
console.log(` - ${skill.skillName} (similarity: ${skill.similarity.toFixed(3)})`);
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Return functions from relevant skills
|
|
116
|
+
return relevantSkills.flatMap(skill => skill.functions);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Extract intent from playbook content
|
|
121
|
+
*/
|
|
122
|
+
extractIntent(playbookContent) {
|
|
123
|
+
// Remove template literals and extract meaningful sentences
|
|
124
|
+
const cleanText = playbookContent
|
|
125
|
+
.replace(/\$\{[^}]+\}/g, '') // Remove ${...}
|
|
126
|
+
.replace(/IMPORTANT:.*/gi, '') // Remove "IMPORTANT" instructions
|
|
127
|
+
.replace(/Return.*JSON.*/gi, '') // Remove JSON format instructions
|
|
128
|
+
.split('\n')
|
|
129
|
+
.map(line => line.trim())
|
|
130
|
+
.filter(line => line.length > 0 && !line.startsWith('//'))
|
|
131
|
+
.slice(0, 3) // Take first 3 meaningful lines
|
|
132
|
+
.join(' ')
|
|
133
|
+
.trim();
|
|
134
|
+
|
|
135
|
+
return cleanText || playbookContent.substring(0, 200);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Get embedding for text (with lazy initialization)
|
|
140
|
+
*/
|
|
141
|
+
async getEmbedding(text) {
|
|
142
|
+
if (!this.embeddingProvider) {
|
|
143
|
+
this.embeddingProvider = new LLMProvider({
|
|
144
|
+
provider: 'openai',
|
|
145
|
+
model: 'text-embedding-3-small'
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return await this.embeddingProvider.getEmbedding(text);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Calculate cosine similarity between two vectors
|
|
154
|
+
*/
|
|
155
|
+
cosineSimilarity(vecA, vecB) {
|
|
156
|
+
if (!vecA || !vecB || vecA.length !== vecB.length) {
|
|
157
|
+
return 0;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const dotProduct = vecA.reduce((sum, a, i) => sum + a * vecB[i], 0);
|
|
161
|
+
const magnitudeA = Math.sqrt(vecA.reduce((sum, a) => sum + a * a, 0));
|
|
162
|
+
const magnitudeB = Math.sqrt(vecB.reduce((sum, b) => sum + b * b, 0));
|
|
163
|
+
|
|
164
|
+
if (magnitudeA === 0 || magnitudeB === 0) {
|
|
165
|
+
return 0;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return dotProduct / (magnitudeA * magnitudeB);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Global skill selector instance
|
|
173
|
+
export const skillSelector = new SkillSelector({ verbose: false });
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export class Skill {
|
|
2
|
+
constructor(config) {
|
|
3
|
+
this.name = config.name;
|
|
4
|
+
this.affordance = config.affordance || '';
|
|
5
|
+
this.run = config.run || (async () => ({ error: 'Not implemented' }));
|
|
6
|
+
this.agents = config.agents || {};
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
async execute(input) {
|
|
10
|
+
console.log(`[Skill:${this.name}] Executing with input:`, input);
|
|
11
|
+
|
|
12
|
+
try {
|
|
13
|
+
const result = await this.run(input);
|
|
14
|
+
console.log(`[Skill:${this.name}] Completed successfully`);
|
|
15
|
+
return result;
|
|
16
|
+
} catch (error) {
|
|
17
|
+
console.error(`[Skill:${this.name}] Error:`, error.message);
|
|
18
|
+
return { error: error.message };
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
toString() {
|
|
23
|
+
return `Skill(${this.name})`;
|
|
24
|
+
}
|
|
25
|
+
}
|