@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,447 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build-Time Optimizer
|
|
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
|
+
*
|
|
9
|
+
* Uses SHA-256 content hashing to detect changes and avoid redundant API calls.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { LLMProvider } from '../runtime/llm-provider.js';
|
|
13
|
+
import { CacheManager } from './cache-manager.js';
|
|
14
|
+
|
|
15
|
+
export class BuildTimeOptimizer {
|
|
16
|
+
constructor(config = {}) {
|
|
17
|
+
this.enableCache = config.cache !== false;
|
|
18
|
+
this.verbose = config.verbose || false;
|
|
19
|
+
this.llmProvider = null; // For embeddings
|
|
20
|
+
this.chatProvider = null; // For code introspection
|
|
21
|
+
this.cacheManager = new CacheManager({
|
|
22
|
+
verbose: config.verbose || false
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* 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
|
+
*/
|
|
32
|
+
async optimizeAST(ast, sourceContent = '', sourcePath = 'unknown') {
|
|
33
|
+
if (!this.enableCache) {
|
|
34
|
+
if (this.verbose) console.log('[BuildOptimizer] Cache disabled, skipping');
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
console.log('š [BuildOptimizer] Checking cache...');
|
|
39
|
+
|
|
40
|
+
// Check if we have cached data for this exact source content
|
|
41
|
+
const cached = this.cacheManager.get(sourceContent, sourcePath);
|
|
42
|
+
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!`);
|
|
47
|
+
return cached;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Cache miss - generate embeddings
|
|
51
|
+
console.log('š [BuildOptimizer] Cache miss, pre-computing embeddings...');
|
|
52
|
+
|
|
53
|
+
const affordances = {};
|
|
54
|
+
const skillAffordances = {};
|
|
55
|
+
let totalEmbeddings = 0;
|
|
56
|
+
let totalSkillEmbeddings = 0;
|
|
57
|
+
|
|
58
|
+
// Find all declarations in AST
|
|
59
|
+
for (const decl of ast.declarations) {
|
|
60
|
+
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;
|
|
66
|
+
}
|
|
67
|
+
} else if (decl.type === 'SkillDecl') {
|
|
68
|
+
const skillData = await this.extractSkillAffordance(decl);
|
|
69
|
+
|
|
70
|
+
if (skillData) {
|
|
71
|
+
skillAffordances[decl.name.name] = skillData;
|
|
72
|
+
totalSkillEmbeddings++;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
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
|
+
};
|
|
90
|
+
|
|
91
|
+
// Store in cache
|
|
92
|
+
this.cacheManager.set(sourceContent, sourcePath, result);
|
|
93
|
+
|
|
94
|
+
return result;
|
|
95
|
+
}
|
|
96
|
+
|
|
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;
|
|
105
|
+
|
|
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);
|
|
110
|
+
|
|
111
|
+
if (agentAffordances && Object.keys(agentAffordances).length > 0) {
|
|
112
|
+
affordances[decl.name.name] = agentAffordances;
|
|
113
|
+
totalEmbeddings += Object.keys(agentAffordances).length;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
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
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Extract affordances for a single agent
|
|
132
|
+
*/
|
|
133
|
+
async extractAgentAffordances(agentNode) {
|
|
134
|
+
const affordances = {};
|
|
135
|
+
|
|
136
|
+
// Find event handlers
|
|
137
|
+
const eventHandlers = agentNode.body.filter(b => b.type === 'EventHandler');
|
|
138
|
+
|
|
139
|
+
if (eventHandlers.length === 0) {
|
|
140
|
+
return affordances;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (this.verbose) {
|
|
144
|
+
console.log(` [Agent:${agentNode.name.name}] Extracting ${eventHandlers.length} affordances...`);
|
|
145
|
+
}
|
|
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
|
+
}
|
|
168
|
+
|
|
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}`;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Generate embedding
|
|
176
|
+
const embedding = await this.generateEmbedding(description);
|
|
177
|
+
|
|
178
|
+
affordances[eventName] = {
|
|
179
|
+
description: description,
|
|
180
|
+
embedding: embedding,
|
|
181
|
+
confidence: confidence,
|
|
182
|
+
hasPlaybook: hasPlaybook
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
if (this.verbose) {
|
|
186
|
+
console.log(` ā ${eventName}: "${description.substring(0, 50)}..."`);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return affordances;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Find playbook for an event handler
|
|
195
|
+
*/
|
|
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
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return null;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Infer intent from playbook text
|
|
209
|
+
*/
|
|
210
|
+
inferIntentFromPlaybook(playbook, eventName) {
|
|
211
|
+
// Handle non-string playbooks
|
|
212
|
+
if (!playbook || typeof playbook !== 'string') {
|
|
213
|
+
return this.humanizeEventName(eventName);
|
|
214
|
+
}
|
|
215
|
+
|
|
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;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Fallback
|
|
230
|
+
return this.humanizeEventName(eventName);
|
|
231
|
+
}
|
|
232
|
+
|
|
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 = [];
|
|
242
|
+
|
|
243
|
+
for (const stmt of handler.body) {
|
|
244
|
+
const code = this.serializeStatement(stmt);
|
|
245
|
+
if (code) {
|
|
246
|
+
codeLines.push(code);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const sourceCode = codeLines.join('\n');
|
|
251
|
+
|
|
252
|
+
// If no code, fallback to event name
|
|
253
|
+
if (!sourceCode || sourceCode.trim().length === 0) {
|
|
254
|
+
return this.humanizeEventName(eventName);
|
|
255
|
+
}
|
|
256
|
+
|
|
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
|
|
275
|
+
|
|
276
|
+
Return ONLY the description text, no markdown, no explanations, no prefix like "This handler...". Just a direct description.`;
|
|
277
|
+
|
|
278
|
+
try {
|
|
279
|
+
const response = await this.chatProvider.executeOpenAI(introspectionPrompt, false);
|
|
280
|
+
|
|
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();
|
|
286
|
+
|
|
287
|
+
if (description && description.length > 10) {
|
|
288
|
+
return description;
|
|
289
|
+
}
|
|
290
|
+
} catch (error) {
|
|
291
|
+
console.warn(`[BuildOptimizer] Code introspection failed for ${eventName}:`, error.message);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Fallback to humanized event name
|
|
295
|
+
return this.humanizeEventName(eventName);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Serialize an AST statement to source code (simplified)
|
|
300
|
+
*/
|
|
301
|
+
serializeStatement(stmt) {
|
|
302
|
+
if (!stmt) return '';
|
|
303
|
+
|
|
304
|
+
switch (stmt.type) {
|
|
305
|
+
case 'ConstDeclaration':
|
|
306
|
+
return `const ${stmt.name.name} = ...`;
|
|
307
|
+
|
|
308
|
+
case 'ReturnStatement':
|
|
309
|
+
if (stmt.value && stmt.value.type === 'ObjectLiteral') {
|
|
310
|
+
// Extract object keys
|
|
311
|
+
const keys = stmt.value.properties?.map(p => p.key?.name || p.key).join(', ') || '';
|
|
312
|
+
return `return { ${keys} }`;
|
|
313
|
+
}
|
|
314
|
+
return 'return ...';
|
|
315
|
+
|
|
316
|
+
case 'SendStatement':
|
|
317
|
+
const role = stmt.role?.name || 'Role';
|
|
318
|
+
const event = stmt.event?.name || 'event';
|
|
319
|
+
return `send to ${role}.${event}()`;
|
|
320
|
+
|
|
321
|
+
case 'ExpressionStatement':
|
|
322
|
+
if (stmt.expression?.type === 'CallExpression') {
|
|
323
|
+
const callee = stmt.expression.callee?.name || stmt.expression.callee?.property?.name || 'function';
|
|
324
|
+
return `${callee}(...)`;
|
|
325
|
+
}
|
|
326
|
+
return '...';
|
|
327
|
+
|
|
328
|
+
default:
|
|
329
|
+
return `// ${stmt.type}`;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
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
|
+
humanizeEventName(eventName) {
|
|
381
|
+
return eventName
|
|
382
|
+
.replace(/([A-Z])/g, ' $1')
|
|
383
|
+
.replace(/_/g, ' ')
|
|
384
|
+
.toLowerCase()
|
|
385
|
+
.trim();
|
|
386
|
+
}
|
|
387
|
+
|
|
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
|
+
}
|
|
399
|
+
|
|
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
|
+
}
|
|
410
|
+
|
|
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;
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* Generate JavaScript code for cached data
|
|
421
|
+
*/
|
|
422
|
+
generateCacheCode(cacheData) {
|
|
423
|
+
if (!cacheData) {
|
|
424
|
+
return '';
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
const totalAffordances = (cacheData.metadata.totalAffordances || 0) + (cacheData.metadata.totalSkillAffordances || 0);
|
|
428
|
+
|
|
429
|
+
const code = `
|
|
430
|
+
// ============================================================
|
|
431
|
+
// Pre-computed Affordances (Build-time Cache)
|
|
432
|
+
// Generated at: ${new Date(cacheData.metadata.generatedAt).toISOString()}
|
|
433
|
+
// Total agents: ${cacheData.metadata.totalAgents || 0}
|
|
434
|
+
// Total agent affordances: ${cacheData.metadata.totalAffordances || 0}
|
|
435
|
+
// Total skills: ${cacheData.metadata.totalSkills || 0}
|
|
436
|
+
// Total skill affordances: ${cacheData.metadata.totalSkillAffordances || 0}
|
|
437
|
+
// ============================================================
|
|
438
|
+
|
|
439
|
+
const CACHED_AFFORDANCES = ${JSON.stringify(cacheData.affordances || {}, null, 2)};
|
|
440
|
+
|
|
441
|
+
const CACHED_SKILL_AFFORDANCES = ${JSON.stringify(cacheData.skillAffordances || {}, null, 2)};
|
|
442
|
+
|
|
443
|
+
`;
|
|
444
|
+
|
|
445
|
+
return code;
|
|
446
|
+
}
|
|
447
|
+
}
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Persistent Cache Manager
|
|
3
|
+
*
|
|
4
|
+
* Manages a persistent cache directory for build-time optimizations.
|
|
5
|
+
* Uses SHA-256 hashing to detect source file changes and avoid
|
|
6
|
+
* regenerating expensive computations (embeddings, etc.)
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import fs from 'fs';
|
|
10
|
+
import path from 'path';
|
|
11
|
+
import crypto from 'crypto';
|
|
12
|
+
import { fileURLToPath } from 'url';
|
|
13
|
+
|
|
14
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
15
|
+
const __dirname = path.dirname(__filename);
|
|
16
|
+
|
|
17
|
+
export class CacheManager {
|
|
18
|
+
constructor(config = {}) {
|
|
19
|
+
// Cache directory relative to project root
|
|
20
|
+
this.cacheDir = config.cacheDir || path.join(process.cwd(), '.koi-cache');
|
|
21
|
+
this.verbose = config.verbose || false;
|
|
22
|
+
|
|
23
|
+
// Ensure cache directory exists
|
|
24
|
+
this.ensureCacheDir();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Ensure cache directory exists
|
|
29
|
+
*/
|
|
30
|
+
ensureCacheDir() {
|
|
31
|
+
if (!fs.existsSync(this.cacheDir)) {
|
|
32
|
+
fs.mkdirSync(this.cacheDir, { recursive: true });
|
|
33
|
+
if (this.verbose) {
|
|
34
|
+
console.log(`[Cache] Created cache directory: ${this.cacheDir}`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Compute SHA-256 hash of file content
|
|
41
|
+
*/
|
|
42
|
+
hashContent(content) {
|
|
43
|
+
return crypto.createHash('sha256').update(content).digest('hex');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Get cache file path for a source file
|
|
48
|
+
*/
|
|
49
|
+
getCachePath(sourceHash) {
|
|
50
|
+
return path.join(this.cacheDir, `affordances-${sourceHash}.json`);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Get metadata file path
|
|
55
|
+
*/
|
|
56
|
+
getMetadataPath() {
|
|
57
|
+
return path.join(this.cacheDir, 'cache-metadata.json');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Load cache metadata (tracks all cached files)
|
|
62
|
+
*/
|
|
63
|
+
loadMetadata() {
|
|
64
|
+
const metaPath = this.getMetadataPath();
|
|
65
|
+
|
|
66
|
+
if (!fs.existsSync(metaPath)) {
|
|
67
|
+
return {
|
|
68
|
+
version: '1.0.0',
|
|
69
|
+
files: {}
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
try {
|
|
74
|
+
const content = fs.readFileSync(metaPath, 'utf-8');
|
|
75
|
+
return JSON.parse(content);
|
|
76
|
+
} catch (error) {
|
|
77
|
+
console.warn(`[Cache] Failed to load metadata: ${error.message}`);
|
|
78
|
+
return {
|
|
79
|
+
version: '1.0.0',
|
|
80
|
+
files: {}
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Save cache metadata
|
|
87
|
+
*/
|
|
88
|
+
saveMetadata(metadata) {
|
|
89
|
+
const metaPath = this.getMetadataPath();
|
|
90
|
+
|
|
91
|
+
try {
|
|
92
|
+
fs.writeFileSync(metaPath, JSON.stringify(metadata, null, 2));
|
|
93
|
+
} catch (error) {
|
|
94
|
+
console.warn(`[Cache] Failed to save metadata: ${error.message}`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Check if cache exists for source content
|
|
100
|
+
*/
|
|
101
|
+
has(sourceContent) {
|
|
102
|
+
const hash = this.hashContent(sourceContent);
|
|
103
|
+
const cachePath = this.getCachePath(hash);
|
|
104
|
+
return fs.existsSync(cachePath);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Get cached data for source content
|
|
109
|
+
*/
|
|
110
|
+
get(sourceContent, sourcePath = null) {
|
|
111
|
+
const hash = this.hashContent(sourceContent);
|
|
112
|
+
const cachePath = this.getCachePath(hash);
|
|
113
|
+
|
|
114
|
+
if (!fs.existsSync(cachePath)) {
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
try {
|
|
119
|
+
const content = fs.readFileSync(cachePath, 'utf-8');
|
|
120
|
+
const cached = JSON.parse(content);
|
|
121
|
+
|
|
122
|
+
if (this.verbose) {
|
|
123
|
+
console.log(`[Cache] ā Cache hit for ${sourcePath || 'source'} (hash: ${hash.substring(0, 8)}...)`);
|
|
124
|
+
console.log(`[Cache] Cached: ${cached.metadata.totalAffordances} affordances from ${new Date(cached.metadata.generatedAt).toLocaleString()}`);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return cached;
|
|
128
|
+
} catch (error) {
|
|
129
|
+
console.warn(`[Cache] Failed to read cache: ${error.message}`);
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Store data in cache
|
|
136
|
+
*/
|
|
137
|
+
set(sourceContent, sourcePath, data) {
|
|
138
|
+
const hash = this.hashContent(sourceContent);
|
|
139
|
+
const cachePath = this.getCachePath(hash);
|
|
140
|
+
|
|
141
|
+
// Add cache metadata
|
|
142
|
+
const cacheEntry = {
|
|
143
|
+
...data,
|
|
144
|
+
cacheMetadata: {
|
|
145
|
+
sourceHash: hash,
|
|
146
|
+
sourcePath: sourcePath,
|
|
147
|
+
cachedAt: Date.now()
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
try {
|
|
152
|
+
fs.writeFileSync(cachePath, JSON.stringify(cacheEntry, null, 2));
|
|
153
|
+
|
|
154
|
+
if (this.verbose) {
|
|
155
|
+
console.log(`[Cache] ā Cached ${data.metadata.totalAffordances} affordances to ${path.basename(cachePath)}`);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Update metadata index
|
|
159
|
+
const metadata = this.loadMetadata();
|
|
160
|
+
metadata.files[sourcePath] = {
|
|
161
|
+
hash: hash,
|
|
162
|
+
lastCached: Date.now(),
|
|
163
|
+
affordanceCount: data.metadata.totalAffordances
|
|
164
|
+
};
|
|
165
|
+
this.saveMetadata(metadata);
|
|
166
|
+
|
|
167
|
+
return true;
|
|
168
|
+
} catch (error) {
|
|
169
|
+
console.warn(`[Cache] Failed to write cache: ${error.message}`);
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Clear cache for a specific file or all cache
|
|
176
|
+
*/
|
|
177
|
+
clear(sourcePath = null) {
|
|
178
|
+
if (!sourcePath) {
|
|
179
|
+
// Clear all cache
|
|
180
|
+
if (fs.existsSync(this.cacheDir)) {
|
|
181
|
+
const files = fs.readdirSync(this.cacheDir);
|
|
182
|
+
files.forEach(file => {
|
|
183
|
+
fs.unlinkSync(path.join(this.cacheDir, file));
|
|
184
|
+
});
|
|
185
|
+
console.log(`[Cache] Cleared all cache (${files.length} files)`);
|
|
186
|
+
}
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Clear cache for specific file
|
|
191
|
+
const metadata = this.loadMetadata();
|
|
192
|
+
const fileInfo = metadata.files[sourcePath];
|
|
193
|
+
|
|
194
|
+
if (fileInfo) {
|
|
195
|
+
const cachePath = this.getCachePath(fileInfo.hash);
|
|
196
|
+
if (fs.existsSync(cachePath)) {
|
|
197
|
+
fs.unlinkSync(cachePath);
|
|
198
|
+
delete metadata.files[sourcePath];
|
|
199
|
+
this.saveMetadata(metadata);
|
|
200
|
+
console.log(`[Cache] Cleared cache for ${sourcePath}`);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Get cache statistics
|
|
207
|
+
*/
|
|
208
|
+
getStats() {
|
|
209
|
+
const metadata = this.loadMetadata();
|
|
210
|
+
const files = Object.keys(metadata.files).length;
|
|
211
|
+
|
|
212
|
+
let totalSize = 0;
|
|
213
|
+
let totalAffordances = 0;
|
|
214
|
+
|
|
215
|
+
if (fs.existsSync(this.cacheDir)) {
|
|
216
|
+
const cacheFiles = fs.readdirSync(this.cacheDir);
|
|
217
|
+
cacheFiles.forEach(file => {
|
|
218
|
+
const filePath = path.join(this.cacheDir, file);
|
|
219
|
+
const stats = fs.statSync(filePath);
|
|
220
|
+
totalSize += stats.size;
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
Object.values(metadata.files).forEach(info => {
|
|
225
|
+
totalAffordances += info.affordanceCount || 0;
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
return {
|
|
229
|
+
cacheDir: this.cacheDir,
|
|
230
|
+
files: files,
|
|
231
|
+
totalAffordances: totalAffordances,
|
|
232
|
+
totalSize: totalSize,
|
|
233
|
+
totalSizeFormatted: this.formatBytes(totalSize)
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Format bytes to human readable
|
|
239
|
+
*/
|
|
240
|
+
formatBytes(bytes) {
|
|
241
|
+
if (bytes === 0) return '0 Bytes';
|
|
242
|
+
const k = 1024;
|
|
243
|
+
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
|
244
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
245
|
+
return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i];
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Print cache summary
|
|
250
|
+
*/
|
|
251
|
+
printStats() {
|
|
252
|
+
const stats = this.getStats();
|
|
253
|
+
|
|
254
|
+
console.log(`\nš Cache Statistics:`);
|
|
255
|
+
console.log(` Location: ${stats.cacheDir}`);
|
|
256
|
+
console.log(` Cached files: ${stats.files}`);
|
|
257
|
+
console.log(` Total affordances: ${stats.totalAffordances}`);
|
|
258
|
+
console.log(` Cache size: ${stats.totalSizeFormatted}`);
|
|
259
|
+
|
|
260
|
+
if (stats.files > 0) {
|
|
261
|
+
const metadata = this.loadMetadata();
|
|
262
|
+
console.log(`\n Recent files:`);
|
|
263
|
+
const sorted = Object.entries(metadata.files)
|
|
264
|
+
.sort((a, b) => b[1].lastCached - a[1].lastCached)
|
|
265
|
+
.slice(0, 5);
|
|
266
|
+
|
|
267
|
+
sorted.forEach(([filepath, info]) => {
|
|
268
|
+
const time = new Date(info.lastCached).toLocaleString();
|
|
269
|
+
console.log(` ⢠${path.basename(filepath)} (${info.affordanceCount} affordances, ${time})`);
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
console.log('');
|
|
273
|
+
}
|
|
274
|
+
}
|