@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,273 @@
1
+ /**
2
+ * Automatic Planning System
3
+ *
4
+ * Allows agents to automatically decompose complex tasks into executable steps
5
+ * using LLM-based planning.
6
+ */
7
+
8
+ import { LLMProvider } from './llm-provider.js';
9
+ import { actionRegistry } from './action-registry.js';
10
+
11
+ export class Planner {
12
+ constructor(config) {
13
+ this.name = config.name || 'DefaultPlanner';
14
+ this.llm = config.llm || { provider: 'openai', model: 'gpt-4o-mini', temperature: 0.3 };
15
+ this.maxSteps = config.maxSteps || 10;
16
+ this.allowReplanning = config.allowReplanning !== false;
17
+ this.llmProvider = null;
18
+ }
19
+
20
+ /**
21
+ * Create a plan for a given goal/task
22
+ * @param {string} goal - The goal to plan for
23
+ * @param {object} context - Execution context
24
+ * @param {Agent} agent - The agent that will execute the plan (for permission filtering)
25
+ */
26
+ async createPlan(goal, context = {}, agent = null) {
27
+ console.log(`[Planner:${this.name}] 📋 Creating plan for: ${goal}`);
28
+
29
+ if (!this.llmProvider) {
30
+ this.llmProvider = new LLMProvider(this.llm);
31
+ }
32
+
33
+ const planningPrompt = this.buildPlanningPrompt(goal, context, agent);
34
+
35
+ try {
36
+ const plan = await this.llmProvider.executePlanning(planningPrompt);
37
+
38
+ if (!plan || !plan.steps || !Array.isArray(plan.steps)) {
39
+ throw new Error('Invalid plan format: missing steps array');
40
+ }
41
+
42
+ console.log(`[Planner:${this.name}] ✓ Created plan with ${plan.steps.length} steps`);
43
+
44
+ return {
45
+ goal,
46
+ steps: plan.steps,
47
+ context: plan.context || {},
48
+ created_at: Date.now()
49
+ };
50
+ } catch (error) {
51
+ console.error(`[Planner:${this.name}] ✗ Planning failed:`, error.message);
52
+ throw new Error(`Planning failed: ${error.message}`);
53
+ }
54
+ }
55
+
56
+ /**
57
+ * Replan after a step failure
58
+ */
59
+ async replan(originalGoal, failedStep, executedSteps, error, context = {}, agent = null) {
60
+ if (!this.allowReplanning) {
61
+ throw new Error('Re-planning is disabled');
62
+ }
63
+
64
+ console.log(`[Planner:${this.name}] 🔄 Re-planning after failure at step ${failedStep}`);
65
+
66
+ const replanPrompt = this.buildReplanningPrompt(
67
+ originalGoal,
68
+ executedSteps,
69
+ failedStep,
70
+ error,
71
+ context,
72
+ agent
73
+ );
74
+
75
+ if (!this.llmProvider) {
76
+ this.llmProvider = new LLMProvider(this.llm);
77
+ }
78
+
79
+ const newPlan = await this.llmProvider.executePlanning(replanPrompt);
80
+
81
+ if (!newPlan || !newPlan.steps || !Array.isArray(newPlan.steps)) {
82
+ throw new Error('Invalid re-plan format: missing steps array');
83
+ }
84
+
85
+ console.log(`[Planner:${this.name}] ✓ Created new plan with ${newPlan.steps.length} steps`);
86
+
87
+ return {
88
+ goal: originalGoal,
89
+ steps: newPlan.steps,
90
+ context: newPlan.context || context,
91
+ replanned: true,
92
+ replanned_at: Date.now()
93
+ };
94
+ }
95
+
96
+ /**
97
+ * Build the planning prompt
98
+ */
99
+ buildPlanningPrompt(goal, context, agent = null) {
100
+ return `Break down this goal into executable steps.
101
+
102
+ Goal: ${goal}
103
+
104
+ ${actionRegistry.generatePromptDocumentation(agent)}
105
+
106
+ Return ONLY JSON (no markdown):
107
+ {
108
+ "steps": [
109
+ { "intent": "action_name", ... }
110
+ ]
111
+ }`;
112
+ }
113
+
114
+ /**
115
+ * Build the re-planning prompt
116
+ */
117
+ buildReplanningPrompt(goal, executedSteps, failedStep, error, context, agent = null) {
118
+ return `Plan failed at step ${failedStep}. Create recovery plan.
119
+
120
+ Goal: ${goal}
121
+ Error: ${error}
122
+
123
+ ${actionRegistry.generatePromptDocumentation(agent)}
124
+
125
+ Return ONLY JSON (no markdown):
126
+ {
127
+ "steps": [
128
+ { "intent": "action_name", ... }
129
+ ]
130
+ }`;
131
+ }
132
+ }
133
+
134
+ /**
135
+ * Planning-enabled Agent
136
+ * Extends regular agents with automatic planning capabilities
137
+ */
138
+ export class PlanningAgent {
139
+ constructor(agent, plannerConfig) {
140
+ this.agent = agent;
141
+ this.planner = new Planner(plannerConfig);
142
+ this.currentPlan = null;
143
+ this.executionHistory = [];
144
+ }
145
+
146
+ /**
147
+ * Execute a goal with automatic planning
148
+ */
149
+ async executeWithPlanning(goal, context = {}) {
150
+ console.log(`[PlanningAgent:${this.agent.name}] 🎯 Starting planned execution`);
151
+ console.log(`[PlanningAgent:${this.agent.name}] Goal: ${goal}`);
152
+
153
+ // Create initial plan (pass the agent for permission filtering)
154
+ this.currentPlan = await this.planner.createPlan(goal, context, this.agent);
155
+ this.executionHistory = [];
156
+
157
+ let stepIndex = 0;
158
+ let retryCount = 0;
159
+ const maxRetries = 2;
160
+
161
+ while (stepIndex < this.currentPlan.steps.length) {
162
+ const step = this.currentPlan.steps[stepIndex];
163
+
164
+ console.log(`[PlanningAgent:${this.agent.name}] 📍 Step ${stepIndex + 1}/${this.currentPlan.steps.length}: ${step.description || step.type}`);
165
+
166
+ try {
167
+ const result = await this.executeStep(step, context);
168
+
169
+ this.executionHistory.push({
170
+ step: stepIndex,
171
+ action: step,
172
+ result,
173
+ success: true
174
+ });
175
+
176
+ // Update context with result
177
+ if (result && typeof result === 'object') {
178
+ context = { ...context, ...result };
179
+ }
180
+
181
+ stepIndex++;
182
+ retryCount = 0;
183
+
184
+ } catch (error) {
185
+ console.error(`[PlanningAgent:${this.agent.name}] ✗ Step ${stepIndex + 1} failed:`, error.message);
186
+
187
+ this.executionHistory.push({
188
+ step: stepIndex,
189
+ action: step,
190
+ error: error.message,
191
+ success: false
192
+ });
193
+
194
+ // Try re-planning
195
+ if (retryCount < maxRetries && this.planner.allowReplanning) {
196
+ retryCount++;
197
+ console.log(`[PlanningAgent:${this.agent.name}] 🔄 Attempting re-plan (${retryCount}/${maxRetries})`);
198
+
199
+ try {
200
+ this.currentPlan = await this.planner.replan(
201
+ goal,
202
+ stepIndex,
203
+ this.executionHistory.filter(h => h.success),
204
+ error.message,
205
+ context,
206
+ this.agent
207
+ );
208
+
209
+ // Restart from beginning of new plan
210
+ stepIndex = 0;
211
+
212
+ } catch (replanError) {
213
+ console.error(`[PlanningAgent:${this.agent.name}] ✗ Re-planning failed:`, replanError.message);
214
+ throw new Error(`Planning failed: ${error.message}. Re-planning also failed: ${replanError.message}`);
215
+ }
216
+ } else {
217
+ throw new Error(`Step ${stepIndex + 1} failed after ${retryCount} retries: ${error.message}`);
218
+ }
219
+ }
220
+ }
221
+
222
+ console.log(`[PlanningAgent:${this.agent.name}] ✓ Plan execution complete`);
223
+
224
+ // Return final result from last step
225
+ const lastHistory = this.executionHistory[this.executionHistory.length - 1];
226
+ return lastHistory?.result || { success: true };
227
+ }
228
+
229
+ /**
230
+ * Execute a single step from the plan
231
+ */
232
+ async executeStep(step, context) {
233
+ // Delegate to the agent's action execution system
234
+ if (this.agent.executeActions) {
235
+ return await this.agent.executeActions([step]);
236
+ }
237
+
238
+ // Fallback: execute based on step type
239
+ switch (step.type) {
240
+ case 'call_skill':
241
+ return await this.agent.callSkill(step.skill, step.input);
242
+
243
+ case 'send_message':
244
+ case 'update_state':
245
+ // Use action registry executor
246
+ const actionDef = actionRegistry.get(step.type);
247
+ if (actionDef && actionDef.execute) {
248
+ return await actionDef.execute(step, this.agent);
249
+ }
250
+ throw new Error(`Action type "${step.type}" has no executor registered`);
251
+
252
+ case 'return':
253
+ return step.data || step.result || {};
254
+
255
+ default:
256
+ throw new Error(`Unknown step type: ${step.type}`);
257
+ }
258
+ }
259
+
260
+ /**
261
+ * Get execution summary
262
+ */
263
+ getSummary() {
264
+ return {
265
+ goal: this.currentPlan?.goal,
266
+ totalSteps: this.currentPlan?.steps.length,
267
+ executedSteps: this.executionHistory.length,
268
+ successfulSteps: this.executionHistory.filter(h => h.success).length,
269
+ failedSteps: this.executionHistory.filter(h => !h.success).length,
270
+ replanned: this.currentPlan?.replanned || false
271
+ };
272
+ }
273
+ }
@@ -0,0 +1,215 @@
1
+ /**
2
+ * Keyv + SQLite Backend for Registry
3
+ *
4
+ * Uses Keyv with SQLite adapter for persistent, queryable storage.
5
+ * Production-ready with transaction support and efficient queries.
6
+ */
7
+
8
+ import Keyv from 'keyv';
9
+ import KeyvSqlite from '@keyv/sqlite';
10
+ import fs from 'fs';
11
+ import path from 'path';
12
+
13
+ export default class KeyvSqliteBackend {
14
+ constructor(options = {}) {
15
+ this.dbPath = options.path || '.koi-registry/registry.sqlite';
16
+ this.keyv = null;
17
+ this.namespace = options.namespace || 'koi';
18
+ this._keysCache = new Set();
19
+ }
20
+
21
+ async init() {
22
+ // Ensure directory exists
23
+ const dbDir = path.dirname(this.dbPath);
24
+ if (!fs.existsSync(dbDir)) {
25
+ fs.mkdirSync(dbDir, { recursive: true });
26
+ }
27
+
28
+ // Initialize Keyv with SQLite adapter
29
+ this.keyv = new Keyv({
30
+ store: new KeyvSqlite(`sqlite://${this.dbPath}`),
31
+ namespace: this.namespace
32
+ });
33
+
34
+ // Handle errors
35
+ this.keyv.on('error', err => {
36
+ console.error('[Registry:KeyvSQLite] Connection error:', err);
37
+ });
38
+ }
39
+
40
+ async get(key) {
41
+ const value = await this.keyv.get(key);
42
+ return value !== undefined ? value : null;
43
+ }
44
+
45
+ async set(key, value) {
46
+ await this.keyv.set(key, value);
47
+ this._keysCache.add(key);
48
+ }
49
+
50
+ async delete(key) {
51
+ const existed = await this.keyv.has(key);
52
+ await this.keyv.delete(key);
53
+ this._keysCache.delete(key);
54
+ return existed;
55
+ }
56
+
57
+ async has(key) {
58
+ return await this.keyv.has(key);
59
+ }
60
+
61
+ async keys(prefix = '') {
62
+ // Return keys from in-memory cache
63
+ // Cache is maintained by set/delete operations
64
+ const keys = Array.from(this._keysCache);
65
+
66
+ if (!prefix) {
67
+ return keys;
68
+ }
69
+
70
+ return keys.filter(key => key.startsWith(prefix));
71
+ }
72
+
73
+ async search(query) {
74
+ // Scan all possible keys by trying common patterns
75
+ // This is a workaround since Keyv doesn't provide a native keys() method
76
+
77
+ // Start with cached keys, then expand the search
78
+ const keysToCheck = new Set(this._keysCache);
79
+ const results = [];
80
+
81
+ // Get values for all known keys
82
+ for (const key of keysToCheck) {
83
+ const value = await this.keyv.get(key);
84
+
85
+ if (value !== undefined) {
86
+ if (this.matchesQuery(value, query)) {
87
+ results.push({ key, value });
88
+ }
89
+ } else {
90
+ // Key no longer exists, remove from cache
91
+ this._keysCache.delete(key);
92
+ }
93
+ }
94
+
95
+ return results;
96
+ }
97
+
98
+ matchesQuery(obj, query) {
99
+ // Handle null/undefined
100
+ if (obj === null || obj === undefined) {
101
+ return false;
102
+ }
103
+
104
+ // Empty query matches all
105
+ if (Object.keys(query).length === 0) {
106
+ return true;
107
+ }
108
+
109
+ // Query must be an object
110
+ if (typeof query !== 'object' || query === null) {
111
+ return false;
112
+ }
113
+
114
+ // Check all query conditions
115
+ for (const [field, condition] of Object.entries(query)) {
116
+ const fieldValue = this.getNestedValue(obj, field);
117
+
118
+ // Direct value comparison
119
+ if (typeof condition !== 'object' || condition === null) {
120
+ if (fieldValue !== condition) {
121
+ return false;
122
+ }
123
+ continue;
124
+ }
125
+
126
+ // Operator-based comparison
127
+ for (const [operator, value] of Object.entries(condition)) {
128
+ switch (operator) {
129
+ case '$eq':
130
+ if (fieldValue !== value) return false;
131
+ break;
132
+
133
+ case '$ne':
134
+ if (fieldValue === value) return false;
135
+ break;
136
+
137
+ case '$gt':
138
+ if (fieldValue <= value) return false;
139
+ break;
140
+
141
+ case '$gte':
142
+ if (fieldValue < value) return false;
143
+ break;
144
+
145
+ case '$lt':
146
+ if (fieldValue >= value) return false;
147
+ break;
148
+
149
+ case '$lte':
150
+ if (fieldValue > value) return false;
151
+ break;
152
+
153
+ case '$in':
154
+ if (!Array.isArray(value) || !value.includes(fieldValue)) return false;
155
+ break;
156
+
157
+ case '$regex':
158
+ const regex = new RegExp(value);
159
+ if (!regex.test(String(fieldValue))) return false;
160
+ break;
161
+
162
+ default:
163
+ console.warn(`[Registry:KeyvSQLite] Unknown operator: ${operator}`);
164
+ return false;
165
+ }
166
+ }
167
+ }
168
+
169
+ return true;
170
+ }
171
+
172
+ getNestedValue(obj, path) {
173
+ // Support dot notation: 'user.name' -> obj.user.name
174
+ const parts = path.split('.');
175
+ let current = obj;
176
+
177
+ for (const part of parts) {
178
+ if (current === null || current === undefined) {
179
+ return undefined;
180
+ }
181
+ current = current[part];
182
+ }
183
+
184
+ return current;
185
+ }
186
+
187
+ async clear() {
188
+ await this.keyv.clear();
189
+ this._keysCache.clear();
190
+ }
191
+
192
+ async stats() {
193
+ const allKeys = await this.keys();
194
+
195
+ // Get file size
196
+ let size = 0;
197
+ if (fs.existsSync(this.dbPath)) {
198
+ size = fs.statSync(this.dbPath).size;
199
+ }
200
+
201
+ return {
202
+ backend: 'keyv-sqlite',
203
+ count: allKeys.length,
204
+ file: this.dbPath,
205
+ size: size
206
+ };
207
+ }
208
+
209
+ async close() {
210
+ // Keyv handles cleanup automatically
211
+ if (this.keyv && this.keyv.opts.store && this.keyv.opts.store.close) {
212
+ await this.keyv.opts.store.close();
213
+ }
214
+ }
215
+ }