@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.
Files changed (85) hide show
  1. package/QUICKSTART.md +89 -0
  2. package/README.md +545 -0
  3. package/examples/actions-demo.koi +177 -0
  4. package/examples/cache-test.koi +29 -0
  5. package/examples/calculator.koi +61 -0
  6. package/examples/clear-registry.js +33 -0
  7. package/examples/clear-registry.koi +30 -0
  8. package/examples/code-introspection-test.koi +149 -0
  9. package/examples/counter.koi +132 -0
  10. package/examples/delegation-test.koi +52 -0
  11. package/examples/directory-import-test.koi +84 -0
  12. package/examples/hello-world-claude.koi +52 -0
  13. package/examples/hello-world.koi +52 -0
  14. package/examples/hello.koi +24 -0
  15. package/examples/mcp-example.koi +70 -0
  16. package/examples/multi-event-handler-test.koi +144 -0
  17. package/examples/new-import-test.koi +89 -0
  18. package/examples/pipeline.koi +162 -0
  19. package/examples/registry-demo.koi +184 -0
  20. package/examples/registry-playbook-demo.koi +162 -0
  21. package/examples/registry-playbook-email-compositor-2.koi +140 -0
  22. package/examples/registry-playbook-email-compositor.koi +140 -0
  23. package/examples/sentiment.koi +90 -0
  24. package/examples/simple.koi +48 -0
  25. package/examples/skill-import-test.koi +76 -0
  26. package/examples/skills/advanced/index.koi +95 -0
  27. package/examples/skills/math-operations.koi +69 -0
  28. package/examples/skills/string-operations.koi +56 -0
  29. package/examples/task-chaining-demo.koi +244 -0
  30. package/examples/test-await.koi +22 -0
  31. package/examples/test-crypto-sha256.koi +196 -0
  32. package/examples/test-delegation.koi +41 -0
  33. package/examples/test-multi-team-routing.koi +258 -0
  34. package/examples/test-no-handler.koi +35 -0
  35. package/examples/test-npm-import.koi +67 -0
  36. package/examples/test-parse.koi +10 -0
  37. package/examples/test-peers-with-team.koi +59 -0
  38. package/examples/test-permissions-fail.koi +20 -0
  39. package/examples/test-permissions.koi +36 -0
  40. package/examples/test-simple-registry.koi +31 -0
  41. package/examples/test-typescript-import.koi +64 -0
  42. package/examples/test-uses-team-syntax.koi +25 -0
  43. package/examples/test-uses-team.koi +31 -0
  44. package/examples/utils/calculator.test.ts +144 -0
  45. package/examples/utils/calculator.ts +56 -0
  46. package/examples/utils/math-helpers.js +50 -0
  47. package/examples/utils/math-helpers.ts +55 -0
  48. package/examples/web-delegation-demo.koi +165 -0
  49. package/package.json +78 -0
  50. package/src/cli/koi.js +793 -0
  51. package/src/compiler/build-optimizer.js +447 -0
  52. package/src/compiler/cache-manager.js +274 -0
  53. package/src/compiler/import-resolver.js +369 -0
  54. package/src/compiler/parser.js +7542 -0
  55. package/src/compiler/transpiler.js +1105 -0
  56. package/src/compiler/typescript-transpiler.js +148 -0
  57. package/src/grammar/koi.pegjs +767 -0
  58. package/src/runtime/action-registry.js +172 -0
  59. package/src/runtime/actions/call-skill.js +45 -0
  60. package/src/runtime/actions/format.js +115 -0
  61. package/src/runtime/actions/print.js +42 -0
  62. package/src/runtime/actions/registry-delete.js +37 -0
  63. package/src/runtime/actions/registry-get.js +37 -0
  64. package/src/runtime/actions/registry-keys.js +33 -0
  65. package/src/runtime/actions/registry-search.js +34 -0
  66. package/src/runtime/actions/registry-set.js +50 -0
  67. package/src/runtime/actions/return.js +31 -0
  68. package/src/runtime/actions/send-message.js +58 -0
  69. package/src/runtime/actions/update-state.js +36 -0
  70. package/src/runtime/agent.js +1368 -0
  71. package/src/runtime/cli-logger.js +205 -0
  72. package/src/runtime/incremental-json-parser.js +201 -0
  73. package/src/runtime/index.js +33 -0
  74. package/src/runtime/llm-provider.js +1372 -0
  75. package/src/runtime/mcp-client.js +1171 -0
  76. package/src/runtime/planner.js +273 -0
  77. package/src/runtime/registry-backends/keyv-sqlite.js +215 -0
  78. package/src/runtime/registry-backends/local.js +260 -0
  79. package/src/runtime/registry.js +162 -0
  80. package/src/runtime/role.js +14 -0
  81. package/src/runtime/router.js +395 -0
  82. package/src/runtime/runtime.js +113 -0
  83. package/src/runtime/skill-selector.js +173 -0
  84. package/src/runtime/skill.js +25 -0
  85. 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
+ }