@koi-language/koi 1.0.6 → 1.1.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 (113) hide show
  1. package/README.md +4 -125
  2. package/examples/.build/agent-dialogue.ts +138 -0
  3. package/examples/.build/agent-dialogue.ts.map +1 -0
  4. package/examples/.build/chess.ts +77 -0
  5. package/examples/.build/chess.ts.map +1 -0
  6. package/examples/.build/delegation-test.ts +140 -0
  7. package/examples/.build/delegation-test.ts.map +1 -0
  8. package/examples/.build/dialog-demo.ts +77 -0
  9. package/examples/.build/dialog-demo.ts.map +1 -0
  10. package/examples/.build/hello-world.ts +77 -0
  11. package/examples/.build/hello-world.ts.map +1 -0
  12. package/examples/.build/lover-dialog-demo.ts +77 -0
  13. package/examples/.build/lover-dialog-demo.ts.map +1 -0
  14. package/examples/.build/package.json +3 -0
  15. package/examples/.build/registry-interactive-demo.ts +202 -0
  16. package/examples/.build/registry-interactive-demo.ts.map +1 -0
  17. package/examples/.build/registry-playbook-demo.ts +201 -0
  18. package/examples/.build/registry-playbook-demo.ts.map +1 -0
  19. package/examples/.build/tic-tac-toe.ts +77 -0
  20. package/examples/.build/tic-tac-toe.ts.map +1 -0
  21. package/examples/actions-demo.koi +8 -9
  22. package/examples/activists-dialogue.koi +75 -0
  23. package/examples/agent-dialogue.koi +66 -0
  24. package/examples/chess.koi +19 -0
  25. package/examples/counter.koi +20 -69
  26. package/examples/delegation-test.koi +16 -18
  27. package/examples/dialog-demo.koi +20 -0
  28. package/examples/hello-world.koi +7 -43
  29. package/examples/mcp-stdio-demo.koi +29 -0
  30. package/examples/memory-test.koi +49 -0
  31. package/examples/mobile-mcp-demo.koi +32 -0
  32. package/examples/multi-event-handler-test.koi +16 -18
  33. package/examples/pipeline.koi +15 -17
  34. package/examples/prompt-demo.koi +20 -0
  35. package/examples/{registry-playbook-email-compositor.koi → registry-interactive-demo.koi} +27 -27
  36. package/examples/registry-playbook-demo.koi +28 -28
  37. package/examples/skill-import-test.koi +7 -9
  38. package/examples/skills/.build/math-operations.ts +1656 -0
  39. package/examples/skills/.build/math-operations.ts.map +1 -0
  40. package/examples/skills/.build/package.json +3 -0
  41. package/examples/skills/.build/string-operations.ts +1643 -0
  42. package/examples/skills/.build/string-operations.ts.map +1 -0
  43. package/examples/skills/advanced/.build/index.ts +3223 -0
  44. package/examples/skills/advanced/.build/index.ts.map +1 -0
  45. package/examples/skills/advanced/.build/package.json +3 -0
  46. package/examples/skills/advanced/index.koi +3 -5
  47. package/examples/skills/math-operations.koi +1 -3
  48. package/examples/skills/string-operations.koi +1 -3
  49. package/examples/tic-tac-toe.koi +19 -0
  50. package/examples/utils/echo-mcp-server.js +141 -0
  51. package/examples/web-delegation-demo.koi +15 -17
  52. package/package.json +2 -1
  53. package/src/cli/koi.js +30 -41
  54. package/src/compiler/build-optimizer.js +204 -289
  55. package/src/compiler/cache-manager.js +1 -1
  56. package/src/compiler/import-resolver.js +5 -9
  57. package/src/compiler/parser.js +6072 -3476
  58. package/src/compiler/transpiler.js +346 -38
  59. package/src/grammar/koi.pegjs +302 -62
  60. package/src/runtime/actions/{format.js → call-llm.js} +37 -44
  61. package/src/runtime/actions/call-mcp.js +97 -0
  62. package/src/runtime/actions/if.js +179 -0
  63. package/src/runtime/actions/print.js +3 -1
  64. package/src/runtime/actions/prompt-user.js +75 -0
  65. package/src/runtime/actions/repeat.js +147 -0
  66. package/src/runtime/actions/shell.js +185 -0
  67. package/src/runtime/actions/while.js +205 -0
  68. package/src/runtime/agent.js +592 -178
  69. package/src/runtime/cli-display.js +26 -0
  70. package/src/runtime/cli-input.js +421 -0
  71. package/src/runtime/cli-logger.js +2 -5
  72. package/src/runtime/cli-markdown.js +61 -0
  73. package/src/runtime/cli-select.js +106 -0
  74. package/src/runtime/incremental-json-parser.js +27 -17
  75. package/src/runtime/index.js +1 -0
  76. package/src/runtime/llm-provider.js +1083 -572
  77. package/src/runtime/mcp-registry.js +141 -0
  78. package/src/runtime/mcp-stdio-client.js +334 -0
  79. package/src/runtime/planner.js +1 -1
  80. package/src/runtime/playbook-session.js +259 -0
  81. package/src/runtime/registry-backends/keyv-sqlite.js +1 -1
  82. package/src/runtime/registry-backends/local.js +1 -1
  83. package/src/runtime/router.js +22 -26
  84. package/src/runtime/runtime.js +7 -1
  85. package/examples/cache-test.koi +0 -29
  86. package/examples/calculator.koi +0 -61
  87. package/examples/clear-registry.js +0 -33
  88. package/examples/clear-registry.koi +0 -30
  89. package/examples/code-introspection-test.koi +0 -149
  90. package/examples/directory-import-test.koi +0 -84
  91. package/examples/hello-world-claude.koi +0 -52
  92. package/examples/hello.koi +0 -24
  93. package/examples/mcp-example.koi +0 -70
  94. package/examples/new-import-test.koi +0 -89
  95. package/examples/registry-demo.koi +0 -184
  96. package/examples/registry-playbook-email-compositor-2.koi +0 -140
  97. package/examples/sentiment.koi +0 -90
  98. package/examples/simple.koi +0 -48
  99. package/examples/task-chaining-demo.koi +0 -244
  100. package/examples/test-await.koi +0 -22
  101. package/examples/test-crypto-sha256.koi +0 -196
  102. package/examples/test-delegation.koi +0 -41
  103. package/examples/test-multi-team-routing.koi +0 -258
  104. package/examples/test-no-handler.koi +0 -35
  105. package/examples/test-npm-import.koi +0 -67
  106. package/examples/test-parse.koi +0 -10
  107. package/examples/test-peers-with-team.koi +0 -59
  108. package/examples/test-permissions-fail.koi +0 -20
  109. package/examples/test-permissions.koi +0 -36
  110. package/examples/test-simple-registry.koi +0 -31
  111. package/examples/test-typescript-import.koi +0 -64
  112. package/examples/test-uses-team-syntax.koi +0 -25
  113. package/examples/test-uses-team.koi +0 -31
@@ -1,11 +1,8 @@
1
1
  /**
2
2
  * Build-Time Optimizer
3
3
  *
4
- * Pre-computes expensive operations during compilation to avoid runtime overhead:
5
- * - Embeddings for agent affordances (with persistent caching)
6
- * - Static agent metadata
7
- * - Any other cacheable data
8
- *
4
+ * Pre-computes handler descriptions during compilation to avoid runtime overhead.
5
+ * Uses a SINGLE batched LLM call to summarize all handlers at once.
9
6
  * Uses SHA-256 content hashing to detect changes and avoid redundant API calls.
10
7
  */
11
8
 
@@ -16,8 +13,6 @@ export class BuildTimeOptimizer {
16
13
  constructor(config = {}) {
17
14
  this.enableCache = config.cache !== false;
18
15
  this.verbose = config.verbose || false;
19
- this.llmProvider = null; // For embeddings
20
- this.chatProvider = null; // For code introspection
21
16
  this.cacheManager = new CacheManager({
22
17
  verbose: config.verbose || false
23
18
  });
@@ -25,9 +20,6 @@ export class BuildTimeOptimizer {
25
20
 
26
21
  /**
27
22
  * Extract and pre-compute affordances from AST (with cache)
28
- * @param ast - Abstract syntax tree
29
- * @param sourceContent - Original source code content (for cache hashing)
30
- * @param sourcePath - Path to source file (for cache tracking)
31
23
  */
32
24
  async optimizeAST(ast, sourceContent = '', sourcePath = 'unknown') {
33
25
  if (!this.enableCache) {
@@ -35,348 +27,292 @@ export class BuildTimeOptimizer {
35
27
  return null;
36
28
  }
37
29
 
38
- console.log('🔄 [BuildOptimizer] Checking cache...');
30
+ process.stdout.write('\r\x1b[K🔄 Checking cache...');
39
31
 
40
- // Check if we have cached data for this exact source content
41
32
  const cached = this.cacheManager.get(sourceContent, sourcePath);
42
33
  if (cached) {
43
- const totalAffordances = (cached.metadata.totalAffordances || 0) + (cached.metadata.totalSkillAffordances || 0);
44
- console.log(`✅ [BuildOptimizer] Using cached embeddings (${totalAffordances} affordances)`);
45
- console.log(` Last generated: ${new Date(cached.metadata.generatedAt).toLocaleString()}`);
46
- console.log(` 💰 Saved API calls!`);
34
+ process.stdout.write('\r\x1b[K');
47
35
  return cached;
48
36
  }
49
37
 
50
- // Cache miss - generate embeddings
51
- console.log('🔄 [BuildOptimizer] Cache miss, pre-computing embeddings...');
38
+ process.stdout.write('\r\x1b[K🔄 Generating handler descriptions...');
39
+
40
+ const result = await this._generateAllDescriptions(ast);
41
+
42
+ process.stdout.write('\r\x1b[K');
43
+
44
+ this.cacheManager.set(sourceContent, sourcePath, result);
45
+ return result;
46
+ }
47
+
48
+ /**
49
+ * Extract and pre-compute affordances from AST (without cache)
50
+ */
51
+ async optimizeASTWithoutCache(ast) {
52
+ const result = await this._generateAllDescriptions(ast);
53
+ process.stdout.write('\r\x1b[K');
54
+ return result;
55
+ }
52
56
 
57
+ /**
58
+ * Core: collect all handlers, make ONE batched LLM call, distribute results
59
+ */
60
+ async _generateAllDescriptions(ast) {
53
61
  const affordances = {};
54
62
  const skillAffordances = {};
55
- let totalEmbeddings = 0;
56
- let totalSkillEmbeddings = 0;
57
63
 
58
- // Find all declarations in AST
64
+ // 1. Collect all handlers that need descriptions
65
+ const handlerEntries = []; // { agentName, eventName, content, hasPlaybook }
66
+
59
67
  for (const decl of ast.declarations) {
60
68
  if (decl.type === 'AgentDecl') {
61
- const agentAffordances = await this.extractAgentAffordances(decl);
62
-
63
- if (agentAffordances && Object.keys(agentAffordances).length > 0) {
64
- affordances[decl.name.name] = agentAffordances;
65
- totalEmbeddings += Object.keys(agentAffordances).length;
69
+ const agentName = decl.name.name;
70
+ const eventHandlers = decl.body.filter(b => b.type === 'EventHandler');
71
+
72
+ for (const handler of eventHandlers) {
73
+ const eventName = handler.event.name;
74
+ const playbook = this.findPlaybookForHandler(handler);
75
+
76
+ if (playbook) {
77
+ const clean = playbook.replace(/\$\{[^}]+\}/g, '...');
78
+ handlerEntries.push({
79
+ agentName, eventName, content: clean, hasPlaybook: true
80
+ });
81
+ } else {
82
+ const code = this._serializeHandlerCode(handler);
83
+ handlerEntries.push({
84
+ agentName, eventName, content: code || eventName, hasPlaybook: false
85
+ });
86
+ }
66
87
  }
67
88
  } else if (decl.type === 'SkillDecl') {
68
- const skillData = await this.extractSkillAffordance(decl);
69
-
89
+ const skillData = this.extractSkillAffordance(decl);
70
90
  if (skillData) {
71
91
  skillAffordances[decl.name.name] = skillData;
72
- totalSkillEmbeddings++;
73
92
  }
74
93
  }
75
94
  }
76
95
 
77
- console.log(`✅ [BuildOptimizer] Pre-computed ${totalEmbeddings + totalSkillEmbeddings} embeddings (${totalEmbeddings} agents, ${totalSkillEmbeddings} skills)`);
78
-
79
- const result = {
80
- affordances,
81
- skillAffordances,
82
- metadata: {
83
- generatedAt: Date.now(),
84
- totalAgents: Object.keys(affordances).length,
85
- totalAffordances: totalEmbeddings,
86
- totalSkills: Object.keys(skillAffordances).length,
87
- totalSkillAffordances: totalSkillEmbeddings
88
- }
89
- };
96
+ // 2. If no handlers, return empty
97
+ if (handlerEntries.length === 0) {
98
+ return this._buildResult(affordances, skillAffordances);
99
+ }
90
100
 
91
- // Store in cache
92
- this.cacheManager.set(sourceContent, sourcePath, result);
101
+ // 3. Get descriptions via single batched LLM call
102
+ const descriptions = await this._batchSummarize(handlerEntries, ast);
93
103
 
94
- return result;
95
- }
104
+ // 4. Distribute descriptions into affordances map
105
+ for (let i = 0; i < handlerEntries.length; i++) {
106
+ const entry = handlerEntries[i];
107
+ const description = descriptions[i] || this.humanizeEventName(entry.eventName);
96
108
 
97
- /**
98
- * Extract and pre-compute affordances from AST (without cache)
99
- * Generates embeddings but doesn't store them in persistent cache
100
- * @param ast - Abstract syntax tree
101
- */
102
- async optimizeASTWithoutCache(ast) {
103
- const affordances = {};
104
- let totalEmbeddings = 0;
109
+ if (!affordances[entry.agentName]) {
110
+ affordances[entry.agentName] = {};
111
+ }
105
112
 
106
- // Find all agent declarations in AST
107
- for (const decl of ast.declarations) {
108
- if (decl.type === 'AgentDecl') {
109
- const agentAffordances = await this.extractAgentAffordances(decl);
113
+ affordances[entry.agentName][entry.eventName] = {
114
+ description,
115
+ confidence: entry.hasPlaybook ? 0.9 : 0.8,
116
+ hasPlaybook: entry.hasPlaybook
117
+ };
110
118
 
111
- if (agentAffordances && Object.keys(agentAffordances).length > 0) {
112
- affordances[decl.name.name] = agentAffordances;
113
- totalEmbeddings += Object.keys(agentAffordances).length;
114
- }
119
+ if (this.verbose) {
120
+ console.log(` ✓ ${entry.agentName}.${entry.eventName}: "${description.substring(0, 50)}..."`);
115
121
  }
116
122
  }
117
123
 
118
- console.log(`✅ [BuildOptimizer] Pre-computed ${totalEmbeddings} embeddings (no cache)`);
119
-
120
- return {
121
- affordances,
122
- metadata: {
123
- generatedAt: Date.now(),
124
- totalAgents: Object.keys(affordances).length,
125
- totalAffordances: totalEmbeddings
126
- }
127
- };
124
+ return this._buildResult(affordances, skillAffordances);
128
125
  }
129
126
 
130
127
  /**
131
- * Extract affordances for a single agent
128
+ * Single batched LLM call to summarize all handlers at once
132
129
  */
133
- async extractAgentAffordances(agentNode) {
134
- const affordances = {};
130
+ async _batchSummarize(entries, ast) {
131
+ // Pick the first agent's LLM config, or fallback to openai/gpt-4o-mini
132
+ const firstAgentDecl = ast.declarations.find(d => d.type === 'AgentDecl');
133
+ const agentLLM = firstAgentDecl ? this.extractLLMConfig(firstAgentDecl) : null;
134
+ const provider = this.getOrCreateChatProvider(agentLLM);
135
135
 
136
- // Find event handlers
137
- const eventHandlers = agentNode.body.filter(b => b.type === 'EventHandler');
136
+ // Build batch prompt
137
+ const items = entries.map((e, i) => {
138
+ const type = e.hasPlaybook ? 'Playbook' : 'Code';
139
+ return `${i + 1}. Agent "${e.agentName}", handler "${e.eventName}" (${type}):\n${e.content}`;
140
+ }).join('\n\n');
138
141
 
139
- if (eventHandlers.length === 0) {
140
- return affordances;
141
- }
142
+ const prompt = `Summarize each handler below in ONE short sentence (max 15 words each).
143
+ Return one summary per line, numbered to match. Plain English only, no JSON.
142
144
 
143
- if (this.verbose) {
144
- console.log(` [Agent:${agentNode.name.name}] Extracting ${eventHandlers.length} affordances...`);
145
- }
145
+ ${items}`;
146
146
 
147
- for (const handler of eventHandlers) {
148
- const eventName = handler.event.name;
149
-
150
- // Try to find playbook
151
- const playbook = this.findPlaybookForHandler(handler);
152
-
153
- let description;
154
- let confidence;
155
- let hasPlaybook = false;
156
-
157
- if (playbook) {
158
- // Extract description from playbook
159
- description = this.inferIntentFromPlaybook(playbook, eventName);
160
- confidence = 0.9;
161
- hasPlaybook = true;
162
- } else {
163
- // No playbook - use code introspection
164
- description = await this.introspectHandlerCode(handler, eventName);
165
- confidence = 0.8; // High confidence since we analyzed the actual code
166
- hasPlaybook = false;
167
- }
147
+ try {
148
+ const response = await provider.simpleChat(prompt);
168
149
 
169
- // Validate description
170
- if (!description || description.trim() === '') {
171
- console.warn(`⚠️ [BuildOptimizer] Empty description for ${agentNode.name.name}.${eventName}, skipping embedding`);
172
- description = `Handler: ${eventName}`;
150
+ if (!response) {
151
+ return entries.map(e => this._fallbackDescription(e));
173
152
  }
174
153
 
175
- // Generate embedding
176
- const embedding = await this.generateEmbedding(description);
154
+ // Parse numbered lines: "1. description" or "1: description" or just lines
155
+ const lines = response.split('\n')
156
+ .map(l => l.trim())
157
+ .filter(l => l.length > 0);
158
+
159
+ const descriptions = new Array(entries.length);
160
+
161
+ for (const line of lines) {
162
+ // Try to match "N. description" or "N: description"
163
+ const match = line.match(/^(\d+)[.):]\s*(.+)/);
164
+ if (match) {
165
+ const idx = parseInt(match[1]) - 1;
166
+ const desc = match[2].trim();
167
+ if (idx >= 0 && idx < entries.length && desc.length > 3) {
168
+ // Reject JSON responses
169
+ if (!desc.startsWith('{') && !desc.startsWith('[')) {
170
+ descriptions[idx] = desc;
171
+ }
172
+ }
173
+ }
174
+ }
177
175
 
178
- affordances[eventName] = {
179
- description: description,
180
- embedding: embedding,
181
- confidence: confidence,
182
- hasPlaybook: hasPlaybook
183
- };
176
+ // Fill gaps with fallbacks
177
+ for (let i = 0; i < entries.length; i++) {
178
+ if (!descriptions[i]) {
179
+ descriptions[i] = this._fallbackDescription(entries[i]);
180
+ }
181
+ }
184
182
 
183
+ return descriptions;
184
+ } catch (error) {
185
185
  if (this.verbose) {
186
- console.log(` ${eventName}: "${description.substring(0, 50)}..."`);
186
+ console.warn(`[BuildOptimizer] Batch summarization failed: ${error.message}`);
187
187
  }
188
+ return entries.map(e => this._fallbackDescription(e));
188
189
  }
189
-
190
- return affordances;
191
190
  }
192
191
 
193
192
  /**
194
- * Find playbook for an event handler
193
+ * Fallback description when LLM fails
195
194
  */
196
- findPlaybookForHandler(handler) {
197
- // Check if handler has a playbook statement
198
- for (const stmt of handler.body) {
199
- if (stmt.type === 'PlaybookStatement') {
200
- return stmt.content.value;
201
- }
195
+ _fallbackDescription(entry) {
196
+ if (entry.hasPlaybook) {
197
+ const lines = entry.content
198
+ .split('\n')
199
+ .map(l => l.trim())
200
+ .filter(l => l.length > 0 && !l.startsWith('//') && !l.startsWith('Return'))
201
+ .slice(0, 2)
202
+ .join(' ');
203
+ return lines.length > 10 ? lines.substring(0, 100) : this.humanizeEventName(entry.eventName);
202
204
  }
203
-
204
- return null;
205
+ return this.humanizeEventName(entry.eventName);
205
206
  }
206
207
 
207
208
  /**
208
- * Infer intent from playbook text
209
+ * Serialize handler body to readable code summary
209
210
  */
210
- inferIntentFromPlaybook(playbook, eventName) {
211
- // Handle non-string playbooks
212
- if (!playbook || typeof playbook !== 'string') {
213
- return this.humanizeEventName(eventName);
211
+ _serializeHandlerCode(handler) {
212
+ const lines = [];
213
+ for (const stmt of handler.body) {
214
+ const code = this.serializeStatement(stmt);
215
+ if (code) lines.push(code);
214
216
  }
217
+ return lines.join('\n') || null;
218
+ }
215
219
 
216
- // Remove template literals
217
- const cleanText = playbook
218
- .replace(/\$\{[^}]+\}/g, '')
219
- .split('\n')
220
- .map(line => line.trim())
221
- .filter(line => line.length > 0 && !line.startsWith('//') && !line.startsWith('Return'))
222
- .slice(0, 3) // Take first 3 lines
223
- .join(' ');
224
-
225
- if (cleanText.length > 10 && cleanText.length < 200) {
226
- return cleanText;
220
+ _buildResult(affordances, skillAffordances) {
221
+ let totalAffordances = 0;
222
+ for (const agent of Object.values(affordances)) {
223
+ totalAffordances += Object.keys(agent).length;
227
224
  }
228
-
229
- // Fallback
230
- return this.humanizeEventName(eventName);
225
+ return {
226
+ affordances,
227
+ skillAffordances,
228
+ metadata: {
229
+ generatedAt: Date.now(),
230
+ totalAgents: Object.keys(affordances).length,
231
+ totalAffordances,
232
+ totalSkills: Object.keys(skillAffordances).length,
233
+ totalSkillAffordances: Object.keys(skillAffordances).length
234
+ }
235
+ };
231
236
  }
232
237
 
233
- /**
234
- * Introspect handler code using LLM to understand what it does
235
- * @param handler - Event handler AST node
236
- * @param eventName - Name of the event
237
- * @returns Description of what the handler does
238
- */
239
- async introspectHandlerCode(handler, eventName) {
240
- // Serialize the handler body to source code
241
- const codeLines = [];
238
+ // --- Helper methods ---
242
239
 
240
+ findPlaybookForHandler(handler) {
243
241
  for (const stmt of handler.body) {
244
- const code = this.serializeStatement(stmt);
245
- if (code) {
246
- codeLines.push(code);
242
+ if (stmt.type === 'PlaybookStatement') {
243
+ return stmt.content.value;
247
244
  }
248
245
  }
246
+ return null;
247
+ }
249
248
 
250
- const sourceCode = codeLines.join('\n');
249
+ extractLLMConfig(agentNode) {
250
+ const llmConfig = agentNode.body.find(b => b.type === 'LLMConfig');
251
+ if (!llmConfig || !llmConfig.config || !llmConfig.config.properties) return null;
251
252
 
252
- // If no code, fallback to event name
253
- if (!sourceCode || sourceCode.trim().length === 0) {
254
- return this.humanizeEventName(eventName);
253
+ const props = {};
254
+ for (const prop of llmConfig.config.properties) {
255
+ const key = prop.key?.name || prop.key;
256
+ const val = prop.value?.value;
257
+ if (key && val !== undefined) props[key] = val;
255
258
  }
259
+ return props.provider && props.model ? props : null;
260
+ }
256
261
 
257
- // Use LLM to analyze the code
258
- if (!this.chatProvider) {
259
- this.initChatProvider();
260
- }
261
-
262
- const introspectionPrompt = `Analyze this event handler code and provide a concise description (1-2 sentences) of what it does.
263
-
264
- Event name: ${eventName}
265
-
266
- Code:
267
- \`\`\`javascript
268
- ${sourceCode}
269
- \`\`\`
270
-
271
- Focus on:
272
- - What operations it performs
273
- - What data it processes or returns
274
- - Its main purpose or capability
262
+ extractSkillAffordance(skillNode) {
263
+ const affordanceText = skillNode.affordance?.value || skillNode.affordance || '';
275
264
 
276
- Return ONLY the description text, no markdown, no explanations, no prefix like "This handler...". Just a direct description.`;
265
+ if (!affordanceText || affordanceText.trim().length === 0) {
266
+ const description = this.humanizeEventName(skillNode.name.name);
267
+ return { description, confidence: 0.5 };
268
+ }
277
269
 
278
- try {
279
- const response = await this.chatProvider.executeOpenAI(introspectionPrompt, false);
270
+ let cleanDescription = affordanceText
271
+ .split('\n')
272
+ .map(line => line.trim())
273
+ .filter(line => line.length > 0)
274
+ .join(' ')
275
+ .trim();
280
276
 
281
- // Clean up the response
282
- const description = response
283
- .replace(/^(This handler|This event handler|This function|The handler|The function)\s*/i, '')
284
- .replace(/^(handles?|processes?|performs?)\s*/i, '')
285
- .trim();
277
+ if (!cleanDescription || cleanDescription.length === 0) {
278
+ cleanDescription = this.humanizeEventName(skillNode.name.name);
279
+ }
286
280
 
287
- if (description && description.length > 10) {
288
- return description;
289
- }
290
- } catch (error) {
291
- console.warn(`[BuildOptimizer] Code introspection failed for ${eventName}:`, error.message);
281
+ if (this.verbose) {
282
+ console.log(` ✓ Skill ${skillNode.name.name}: "${cleanDescription.substring(0, 50)}..."`);
292
283
  }
293
284
 
294
- // Fallback to humanized event name
295
- return this.humanizeEventName(eventName);
285
+ return { description: cleanDescription, confidence: 0.9 };
296
286
  }
297
287
 
298
- /**
299
- * Serialize an AST statement to source code (simplified)
300
- */
301
288
  serializeStatement(stmt) {
302
289
  if (!stmt) return '';
303
290
 
304
291
  switch (stmt.type) {
305
292
  case 'ConstDeclaration':
306
293
  return `const ${stmt.name.name} = ...`;
307
-
308
294
  case 'ReturnStatement':
309
295
  if (stmt.value && stmt.value.type === 'ObjectLiteral') {
310
- // Extract object keys
311
296
  const keys = stmt.value.properties?.map(p => p.key?.name || p.key).join(', ') || '';
312
297
  return `return { ${keys} }`;
313
298
  }
314
299
  return 'return ...';
315
-
316
- case 'SendStatement':
300
+ case 'SendStatement': {
317
301
  const role = stmt.role?.name || 'Role';
318
302
  const event = stmt.event?.name || 'event';
319
303
  return `send to ${role}.${event}()`;
320
-
304
+ }
321
305
  case 'ExpressionStatement':
322
306
  if (stmt.expression?.type === 'CallExpression') {
323
307
  const callee = stmt.expression.callee?.name || stmt.expression.callee?.property?.name || 'function';
324
308
  return `${callee}(...)`;
325
309
  }
326
310
  return '...';
327
-
328
311
  default:
329
312
  return `// ${stmt.type}`;
330
313
  }
331
314
  }
332
315
 
333
- /**
334
- * Extract affordance for a skill
335
- */
336
- async extractSkillAffordance(skillNode) {
337
- // Skills have an explicit affordance field
338
- const affordanceText = skillNode.affordance?.value || skillNode.affordance || '';
339
-
340
- if (!affordanceText || affordanceText.trim().length === 0) {
341
- // No affordance defined - use skill name as fallback
342
- const description = this.humanizeEventName(skillNode.name.name);
343
- return {
344
- description,
345
- embedding: await this.generateEmbedding(description),
346
- confidence: 0.5
347
- };
348
- }
349
-
350
- // Clean up affordance text (remove extra whitespace/newlines)
351
- let cleanDescription = affordanceText
352
- .split('\n')
353
- .map(line => line.trim())
354
- .filter(line => line.length > 0)
355
- .join(' ')
356
- .trim();
357
-
358
- // Validate description is not empty
359
- if (!cleanDescription || cleanDescription.length === 0) {
360
- cleanDescription = this.humanizeEventName(skillNode.name.name);
361
- }
362
-
363
- // Generate embedding for the affordance
364
- const embedding = await this.generateEmbedding(cleanDescription);
365
-
366
- if (this.verbose) {
367
- console.log(` ✓ Skill ${skillNode.name.name}: "${cleanDescription.substring(0, 50)}..."`);
368
- }
369
-
370
- return {
371
- description: cleanDescription,
372
- embedding: embedding,
373
- confidence: 0.9
374
- };
375
- }
376
-
377
- /**
378
- * Humanize event name
379
- */
380
316
  humanizeEventName(eventName) {
381
317
  return eventName
382
318
  .replace(/([A-Z])/g, ' $1')
@@ -385,48 +321,29 @@ Return ONLY the description text, no markdown, no explanations, no prefix like "
385
321
  .trim();
386
322
  }
387
323
 
388
- /**
389
- * Initialize chat LLM provider for code introspection
390
- */
391
- initChatProvider() {
392
- this.chatProvider = new LLMProvider({
393
- provider: 'openai',
394
- model: 'gpt-4o-mini', // Fast and cheap model for code analysis
395
- temperature: 0.1,
396
- maxTokens: 150
397
- });
398
- }
324
+ getOrCreateChatProvider(agentLLM = null) {
325
+ const provider = agentLLM?.provider || 'openai';
326
+ const model = agentLLM?.model || 'gpt-4o-mini';
327
+ const key = `${provider}:${model}`;
399
328
 
400
- /**
401
- * Generate embedding for text
402
- */
403
- async generateEmbedding(text) {
404
- if (!this.llmProvider) {
405
- this.llmProvider = new LLMProvider({
406
- provider: 'openai',
407
- model: 'text-embedding-3-small'
408
- });
409
- }
329
+ if (!this._chatProviders) this._chatProviders = new Map();
410
330
 
411
- try {
412
- return await this.llmProvider.getEmbedding(text);
413
- } catch (error) {
414
- console.warn(`[BuildOptimizer] Failed to generate embedding: ${error.message}`);
415
- return null;
331
+ if (!this._chatProviders.has(key)) {
332
+ this._chatProviders.set(key, new LLMProvider({
333
+ provider,
334
+ model,
335
+ temperature: 0.1,
336
+ maxTokens: 300 // Enough for batch response
337
+ }));
416
338
  }
339
+
340
+ return this._chatProviders.get(key);
417
341
  }
418
342
 
419
- /**
420
- * Generate JavaScript code for cached data
421
- */
422
343
  generateCacheCode(cacheData) {
423
- if (!cacheData) {
424
- return '';
425
- }
344
+ if (!cacheData) return '';
426
345
 
427
- const totalAffordances = (cacheData.metadata.totalAffordances || 0) + (cacheData.metadata.totalSkillAffordances || 0);
428
-
429
- const code = `
346
+ return `
430
347
  // ============================================================
431
348
  // Pre-computed Affordances (Build-time Cache)
432
349
  // Generated at: ${new Date(cacheData.metadata.generatedAt).toISOString()}
@@ -441,7 +358,5 @@ const CACHED_AFFORDANCES = ${JSON.stringify(cacheData.affordances || {}, null, 2
441
358
  const CACHED_SKILL_AFFORDANCES = ${JSON.stringify(cacheData.skillAffordances || {}, null, 2)};
442
359
 
443
360
  `;
444
-
445
- return code;
446
361
  }
447
362
  }
@@ -17,7 +17,7 @@ const __dirname = path.dirname(__filename);
17
17
  export class CacheManager {
18
18
  constructor(config = {}) {
19
19
  // Cache directory relative to project root
20
- this.cacheDir = config.cacheDir || path.join(process.cwd(), '.koi-cache');
20
+ this.cacheDir = config.cacheDir || path.join(process.cwd(), '.koi', 'cache');
21
21
  this.verbose = config.verbose || false;
22
22
 
23
23
  // Ensure cache directory exists