@koi-language/koi 1.0.5 → 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.
- package/README.md +4 -125
- package/examples/.build/agent-dialogue.ts +138 -0
- package/examples/.build/agent-dialogue.ts.map +1 -0
- package/examples/.build/chess.ts +77 -0
- package/examples/.build/chess.ts.map +1 -0
- package/examples/.build/delegation-test.ts +140 -0
- package/examples/.build/delegation-test.ts.map +1 -0
- package/examples/.build/dialog-demo.ts +77 -0
- package/examples/.build/dialog-demo.ts.map +1 -0
- package/examples/.build/hello-world.ts +77 -0
- package/examples/.build/hello-world.ts.map +1 -0
- package/examples/.build/lover-dialog-demo.ts +77 -0
- package/examples/.build/lover-dialog-demo.ts.map +1 -0
- package/examples/.build/package.json +3 -0
- package/examples/.build/registry-interactive-demo.ts +202 -0
- package/examples/.build/registry-interactive-demo.ts.map +1 -0
- package/examples/.build/registry-playbook-demo.ts +201 -0
- package/examples/.build/registry-playbook-demo.ts.map +1 -0
- package/examples/.build/tic-tac-toe.ts +77 -0
- package/examples/.build/tic-tac-toe.ts.map +1 -0
- package/examples/actions-demo.koi +8 -9
- package/examples/activists-dialogue.koi +75 -0
- package/examples/agent-dialogue.koi +66 -0
- package/examples/chess.koi +19 -0
- package/examples/counter.koi +20 -69
- package/examples/delegation-test.koi +16 -18
- package/examples/dialog-demo.koi +20 -0
- package/examples/hello-world.koi +7 -43
- package/examples/mcp-stdio-demo.koi +29 -0
- package/examples/memory-test.koi +49 -0
- package/examples/mobile-mcp-demo.koi +32 -0
- package/examples/multi-event-handler-test.koi +16 -18
- package/examples/pipeline.koi +15 -17
- package/examples/prompt-demo.koi +20 -0
- package/examples/{registry-playbook-email-compositor.koi → registry-interactive-demo.koi} +27 -27
- package/examples/registry-playbook-demo.koi +28 -28
- package/examples/skill-import-test.koi +7 -9
- package/examples/skills/.build/math-operations.ts +1656 -0
- package/examples/skills/.build/math-operations.ts.map +1 -0
- package/examples/skills/.build/package.json +3 -0
- package/examples/skills/.build/string-operations.ts +1643 -0
- package/examples/skills/.build/string-operations.ts.map +1 -0
- package/examples/skills/advanced/.build/index.ts +3223 -0
- package/examples/skills/advanced/.build/index.ts.map +1 -0
- package/examples/skills/advanced/.build/package.json +3 -0
- package/examples/skills/advanced/index.koi +3 -5
- package/examples/skills/math-operations.koi +1 -3
- package/examples/skills/string-operations.koi +1 -3
- package/examples/tic-tac-toe.koi +19 -0
- package/examples/utils/echo-mcp-server.js +141 -0
- package/examples/web-delegation-demo.koi +15 -17
- package/package.json +2 -1
- package/src/cli/koi.js +30 -41
- package/src/compiler/build-optimizer.js +204 -289
- package/src/compiler/cache-manager.js +1 -1
- package/src/compiler/import-resolver.js +5 -9
- package/src/compiler/parser.js +6072 -3476
- package/src/compiler/transpiler.js +346 -38
- package/src/grammar/koi.pegjs +302 -62
- package/src/runtime/actions/{format.js → call-llm.js} +37 -44
- package/src/runtime/actions/call-mcp.js +97 -0
- package/src/runtime/actions/if.js +179 -0
- package/src/runtime/actions/print.js +3 -1
- package/src/runtime/actions/prompt-user.js +75 -0
- package/src/runtime/actions/repeat.js +147 -0
- package/src/runtime/actions/shell.js +185 -0
- package/src/runtime/actions/while.js +205 -0
- package/src/runtime/agent.js +592 -178
- package/src/runtime/cli-display.js +26 -0
- package/src/runtime/cli-input.js +421 -0
- package/src/runtime/cli-logger.js +2 -5
- package/src/runtime/cli-markdown.js +61 -0
- package/src/runtime/cli-select.js +106 -0
- package/src/runtime/incremental-json-parser.js +51 -17
- package/src/runtime/index.js +1 -0
- package/src/runtime/llm-provider.js +1083 -572
- package/src/runtime/mcp-registry.js +141 -0
- package/src/runtime/mcp-stdio-client.js +334 -0
- package/src/runtime/planner.js +1 -1
- package/src/runtime/playbook-session.js +259 -0
- package/src/runtime/registry-backends/keyv-sqlite.js +1 -1
- package/src/runtime/registry-backends/local.js +1 -1
- package/src/runtime/router.js +22 -26
- package/src/runtime/runtime.js +7 -1
- package/examples/cache-test.koi +0 -29
- package/examples/calculator.koi +0 -61
- package/examples/clear-registry.js +0 -33
- package/examples/clear-registry.koi +0 -30
- package/examples/code-introspection-test.koi +0 -149
- package/examples/directory-import-test.koi +0 -84
- package/examples/hello-world-claude.koi +0 -52
- package/examples/hello.koi +0 -24
- package/examples/mcp-example.koi +0 -70
- package/examples/new-import-test.koi +0 -89
- package/examples/registry-demo.koi +0 -184
- package/examples/registry-playbook-email-compositor-2.koi +0 -140
- package/examples/sentiment.koi +0 -90
- package/examples/simple.koi +0 -48
- package/examples/task-chaining-demo.koi +0 -244
- package/examples/test-await.koi +0 -22
- package/examples/test-crypto-sha256.koi +0 -196
- package/examples/test-delegation.koi +0 -41
- package/examples/test-multi-team-routing.koi +0 -258
- package/examples/test-no-handler.koi +0 -35
- package/examples/test-npm-import.koi +0 -67
- package/examples/test-parse.koi +0 -10
- package/examples/test-peers-with-team.koi +0 -59
- package/examples/test-permissions-fail.koi +0 -20
- package/examples/test-permissions.koi +0 -36
- package/examples/test-simple-registry.koi +0 -31
- package/examples/test-typescript-import.koi +0 -64
- package/examples/test-uses-team-syntax.koi +0 -25
- package/examples/test-uses-team.koi +0 -31
|
@@ -1,11 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Build-Time Optimizer
|
|
3
3
|
*
|
|
4
|
-
* Pre-computes
|
|
5
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
51
|
-
|
|
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
|
-
//
|
|
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
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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 =
|
|
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
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
-
//
|
|
92
|
-
this.
|
|
101
|
+
// 3. Get descriptions via single batched LLM call
|
|
102
|
+
const descriptions = await this._batchSummarize(handlerEntries, ast);
|
|
93
103
|
|
|
94
|
-
|
|
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
|
-
|
|
99
|
-
|
|
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
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
113
|
+
affordances[entry.agentName][entry.eventName] = {
|
|
114
|
+
description,
|
|
115
|
+
confidence: entry.hasPlaybook ? 0.9 : 0.8,
|
|
116
|
+
hasPlaybook: entry.hasPlaybook
|
|
117
|
+
};
|
|
110
118
|
|
|
111
|
-
|
|
112
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
128
|
+
* Single batched LLM call to summarize all handlers at once
|
|
132
129
|
*/
|
|
133
|
-
async
|
|
134
|
-
|
|
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
|
-
//
|
|
137
|
-
const
|
|
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
|
-
|
|
140
|
-
|
|
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
|
-
|
|
144
|
-
console.log(` [Agent:${agentNode.name.name}] Extracting ${eventHandlers.length} affordances...`);
|
|
145
|
-
}
|
|
145
|
+
${items}`;
|
|
146
146
|
|
|
147
|
-
|
|
148
|
-
const
|
|
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
|
-
|
|
170
|
-
|
|
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
|
-
//
|
|
176
|
-
const
|
|
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
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
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.
|
|
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
|
-
*
|
|
193
|
+
* Fallback description when LLM fails
|
|
195
194
|
*/
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
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
|
-
*
|
|
209
|
+
* Serialize handler body to readable code summary
|
|
209
210
|
*/
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
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
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
.
|
|
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
|
-
|
|
230
|
-
|
|
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
|
-
|
|
245
|
-
|
|
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
|
-
|
|
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
|
-
|
|
253
|
-
|
|
254
|
-
|
|
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
|
-
|
|
258
|
-
|
|
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
|
-
|
|
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
|
-
|
|
279
|
-
|
|
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
|
-
|
|
282
|
-
|
|
283
|
-
|
|
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
|
-
|
|
288
|
-
|
|
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
|
-
|
|
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
|
-
|
|
390
|
-
|
|
391
|
-
|
|
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
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
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
|
-
|
|
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
|
|
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
|