@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.
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 +51 -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
@@ -0,0 +1,185 @@
1
+ /**
2
+ * Shell Action - Execute a shell command with user permission.
3
+ *
4
+ * The LLM must provide a human-readable description of what the command does.
5
+ * Before execution, the user is prompted for permission unless the command
6
+ * has been "Always allow"-ed for this agent during the current session.
7
+ *
8
+ * Permission options:
9
+ * - Yes → execute this time only
10
+ * - Always allow → execute without asking again (this agent, this session)
11
+ * - No → skip this time (can be asked again later)
12
+ *
13
+ * Permissions are per-agent and in-memory only (reset between sessions).
14
+ */
15
+
16
+ import { spawn } from 'child_process';
17
+ import { cliLogger } from '../cli-logger.js';
18
+ import { cliSelect } from '../cli-select.js';
19
+
20
+ async function askPermission(command, description) {
21
+ cliLogger.clearProgress();
22
+
23
+ process.stdout.write(`\n${description}\n`);
24
+ process.stdout.write(`🔧 The agent wants to execute this command:\n`);
25
+ process.stdout.write(` \x1b[33m$ ${command}\x1b[0m\n\n`);
26
+
27
+ const value = await cliSelect('Allow this command?', [
28
+ { title: 'Yes', value: 'yes', description: 'Execute this time' },
29
+ { title: 'Always allow', value: 'always', description: 'Always allow for this agent (this session only)' },
30
+ { title: 'No', value: 'no', description: 'Skip this command' }
31
+ ]);
32
+
33
+ return value || 'no';
34
+ }
35
+
36
+ export default {
37
+ type: 'shell',
38
+ intent: 'shell',
39
+ description: 'Execute a shell command (requires user permission). Requires: command (the shell command), description (human-friendly explanation of what it does and why). Optional: cwd (working directory)',
40
+ permission: 'execute',
41
+ hidden: false,
42
+
43
+ schema: {
44
+ type: 'object',
45
+ properties: {
46
+ command: { type: 'string', description: 'The shell command to execute' },
47
+ description: { type: 'string', description: 'Human-friendly reason WHY this command is needed (shown to user). Express NEED, not action. Good: "Need to install X because Y". Bad: "Installing X".' },
48
+ cwd: { type: 'string', description: 'Working directory for the command (optional)' }
49
+ },
50
+ required: ['command', 'description']
51
+ },
52
+
53
+ examples: [
54
+ {
55
+ actionType: 'direct',
56
+ intent: 'shell',
57
+ command: 'npm install',
58
+ description: 'Need to install Node.js dependencies required by the project',
59
+ cwd: '/path/to/project'
60
+ },
61
+ {
62
+ actionType: 'direct',
63
+ intent: 'shell',
64
+ command: 'brew tap facebook/fb && brew install idb-companion && pip install fb-idb',
65
+ description: 'Need to install IDB because the MCP server requires it for iOS Simulator control'
66
+ }
67
+ ],
68
+
69
+ async execute(action, agent) {
70
+ const { command, description, cwd } = action;
71
+
72
+ if (!command) {
73
+ throw new Error('shell: "command" field is required');
74
+ }
75
+ if (!description) {
76
+ throw new Error('shell: "description" field is required');
77
+ }
78
+
79
+ // Reject commands with obvious placeholder values like <your_api_key>, <TOKEN>, etc.
80
+ const placeholderMatch = command.match(/<[a-zA-Z_][a-zA-Z0-9_]*>/);
81
+ if (placeholderMatch) {
82
+ return {
83
+ success: false,
84
+ error: `Command contains a placeholder "${placeholderMatch[0]}" instead of a real value. Do NOT use placeholder values — use actual values or ask the user for them with prompt_user.`
85
+ };
86
+ }
87
+
88
+ // Per-agent in-memory allowed commands (lazy-init on the agent instance)
89
+ if (!agent._allowedShellCommands) {
90
+ agent._allowedShellCommands = new Set();
91
+ }
92
+
93
+ let permitted = agent._allowedShellCommands.has(command);
94
+
95
+ if (permitted) {
96
+ process.stdout.write(`\n${description}\n`);
97
+ process.stdout.write(`⚡ Executing: \x1b[33m$ ${command}\x1b[0m\n`);
98
+ } else {
99
+ // Ask user for permission
100
+ const answer = await askPermission(command, description);
101
+
102
+ if (answer === 'always') {
103
+ agent._allowedShellCommands.add(command);
104
+ permitted = true;
105
+ } else if (answer === 'yes') {
106
+ permitted = true;
107
+ }
108
+ }
109
+
110
+ if (!permitted) {
111
+ console.log(` ⏭️ Skipped`);
112
+ return {
113
+ success: false,
114
+ denied: true,
115
+ message: `User denied execution: ${description}`
116
+ };
117
+ }
118
+
119
+ return new Promise((resolve) => {
120
+ const child = spawn('sh', ['-c', command], {
121
+ cwd: cwd || process.cwd(),
122
+ env: { ...process.env },
123
+ stdio: ['inherit', 'pipe', 'pipe'] // stdin from user (for sudo etc), capture stdout/stderr
124
+ });
125
+
126
+ const stdoutChunks = [];
127
+ const stderrChunks = [];
128
+
129
+ child.stdout.on('data', (data) => {
130
+ stdoutChunks.push(data);
131
+ // Stream stdout to console in real-time
132
+ process.stdout.write(data);
133
+ });
134
+
135
+ child.stderr.on('data', (data) => {
136
+ stderrChunks.push(data);
137
+ // Stream stderr to console in real-time
138
+ process.stderr.write(data);
139
+ });
140
+
141
+ // Timeout after 5 minutes
142
+ const timeout = setTimeout(() => {
143
+ child.kill('SIGTERM');
144
+ }, 300000);
145
+
146
+ child.on('close', (code) => {
147
+ clearTimeout(timeout);
148
+
149
+ const stdoutStr = Buffer.concat(stdoutChunks).toString().trim();
150
+ const stderrStr = Buffer.concat(stderrChunks).toString().trim();
151
+
152
+ if (code !== 0) {
153
+ console.log(` ❌ Failed (exit code ${code})`);
154
+ resolve({
155
+ success: false,
156
+ exitCode: code || 1,
157
+ stdout: stdoutStr,
158
+ stderr: stderrStr,
159
+ error: stderrStr || `Command exited with code ${code}`
160
+ });
161
+ } else {
162
+ console.log(` ✅ Done`);
163
+ resolve({
164
+ success: true,
165
+ exitCode: 0,
166
+ stdout: stdoutStr,
167
+ stderr: stderrStr
168
+ });
169
+ }
170
+ });
171
+
172
+ child.on('error', (err) => {
173
+ clearTimeout(timeout);
174
+ console.log(` ❌ Failed: ${err.message}`);
175
+ resolve({
176
+ success: false,
177
+ exitCode: 1,
178
+ stdout: '',
179
+ stderr: '',
180
+ error: err.message
181
+ });
182
+ });
183
+ });
184
+ }
185
+ };
@@ -0,0 +1,205 @@
1
+ /**
2
+ * While Action - Execute actions at least once, then repeat while condition is true (do-while semantics)
3
+ */
4
+ import { buildActionDisplay } from '../cli-display.js';
5
+
6
+ export default {
7
+ type: 'while',
8
+ intent: 'while',
9
+ description: 'Execute actions at least once, then repeat while condition is true. CONDITION: Use string "${a1.output} !== \'stop\'" for exact match, OR object with llm_eval for semantic: { "llm_eval": true, "instruction": "¿Continuar? (false si despide)", "data": "${a1.output.answer}" } → Returns: { iterations: N, results: [array], stopped_reason: "condition_false" | "max_iterations" }',
10
+ permission: 'execute',
11
+
12
+ schema: {
13
+ type: 'object',
14
+ properties: {
15
+ condition: {
16
+ oneOf: [
17
+ { type: 'string', description: 'Simple condition (e.g., "${a1.output.answer} !== \'hasta luego\'")' },
18
+ { type: 'object', description: 'LLM-evaluated condition with llm_eval: true, instruction, and data fields' }
19
+ ]
20
+ },
21
+ actions: {
22
+ type: 'array',
23
+ description: 'Actions to execute in each iteration'
24
+ },
25
+ max_iterations: {
26
+ type: 'number',
27
+ description: 'Optional safety limit to prevent infinite loops (default: 100)'
28
+ }
29
+ },
30
+ required: ['condition', 'actions']
31
+ },
32
+
33
+ examples: [
34
+ {
35
+ intent: 'while',
36
+ condition: "${a1.output.answer} !== 'hasta luego'",
37
+ max_iterations: 10,
38
+ actions: [
39
+ { id: 'a1', intent: 'prompt_user', question: '¿Qué quieres hablar? (escribe "hasta luego" para salir)' },
40
+ { intent: 'print', message: 'Dijiste: ${a1.output.answer}' }
41
+ ]
42
+ }
43
+ ],
44
+
45
+ // Executor function
46
+ async execute(action, agent) {
47
+ const condition = action.condition || action.data?.condition || '';
48
+ const actions = action.actions || action.data?.actions || [];
49
+ const maxIterations = action.max_iterations || action.data?.max_iterations || 100;
50
+
51
+ if (!condition) {
52
+ throw new Error('while action requires a "condition" field');
53
+ }
54
+
55
+ if (!actions || actions.length === 0) {
56
+ throw new Error('while action requires an "actions" array');
57
+ }
58
+
59
+ // Get the current action context from the agent
60
+ const context = agent._currentActionContext || {
61
+ state: agent.state,
62
+ results: []
63
+ };
64
+
65
+ // Execute actions with inherited context
66
+ const actionRegistry = (await import('../action-registry.js')).actionRegistry;
67
+ const cliLogger = (await import('../cli-logger.js')).cliLogger;
68
+
69
+ const allResults = [];
70
+ let iterations = 0;
71
+ let stoppedReason = 'condition_false';
72
+
73
+ // Do-while loop: execute at least once, then check condition
74
+ while (iterations < maxIterations) {
75
+ iterations++;
76
+
77
+ // Create iteration context with current iteration number
78
+ const iterationContext = {
79
+ ...context,
80
+ iteration: iterations,
81
+ iterationIndex: iterations - 1
82
+ };
83
+
84
+ // Execute actions first
85
+ for (const nestedAction of actions) {
86
+ // Resolve references using the iteration context
87
+ const resolvedAction = agent.resolveActionReferences(nestedAction, iterationContext);
88
+
89
+ // Show progress
90
+ cliLogger.planning(buildActionDisplay(agent.name, resolvedAction));
91
+
92
+ let result;
93
+
94
+ // Check if this is a delegation action
95
+ if (resolvedAction.actionType === 'delegate') {
96
+ // Delegation: route to appropriate team member
97
+ result = await agent.resolveAction(resolvedAction, iterationContext);
98
+ } else {
99
+ // Direct action: Get action definition from registry
100
+ const actionDef = actionRegistry.get(nestedAction.intent || nestedAction.type);
101
+
102
+ if (actionDef && actionDef.execute) {
103
+ // Update agent's current context for nested action
104
+ const previousContext = agent._currentActionContext;
105
+ agent._currentActionContext = iterationContext;
106
+
107
+ // Execute with agent
108
+ result = await actionDef.execute(resolvedAction, agent);
109
+
110
+ // Restore previous context
111
+ agent._currentActionContext = previousContext;
112
+ } else {
113
+ cliLogger.clear();
114
+ throw new Error(`Action ${nestedAction.intent || nestedAction.type} not found`);
115
+ }
116
+ }
117
+
118
+ cliLogger.clear();
119
+
120
+ // Update iteration context with result (and parent context for next iteration's condition check)
121
+ if (result && typeof result === 'object') {
122
+ // Unwrap double-encoded results (LLM sometimes returns { "result": "{...json...}" })
123
+ if (result.result && typeof result.result === 'string' && Object.keys(result).length === 1) {
124
+ try {
125
+ const parsed = JSON.parse(result.result);
126
+ if (typeof parsed === 'object') {
127
+ result = parsed;
128
+ }
129
+ } catch (e) {
130
+ // Not JSON, keep as-is
131
+ }
132
+ }
133
+
134
+ const resultForContext = JSON.parse(JSON.stringify(result));
135
+ iterationContext.results.push(resultForContext);
136
+ context.results.push(resultForContext); // Also update parent for condition evaluation
137
+
138
+ // Store with action ID if provided
139
+ if (nestedAction.id) {
140
+ iterationContext[nestedAction.id] = { output: resultForContext };
141
+ context[nestedAction.id] = { output: resultForContext }; // Also update parent
142
+ }
143
+
144
+ // Track all results
145
+ allResults.push(resultForContext);
146
+ }
147
+ }
148
+
149
+ // After executing actions, evaluate condition to decide if we should continue
150
+ let conditionResult = false;
151
+ try {
152
+ // Check if condition is LLM-evaluated (object with llm_eval: true)
153
+ if (typeof condition === 'object' && condition.llm_eval === true) {
154
+ // Show progress while evaluating condition
155
+ const displayText = condition.desc ? condition.desc.replace(/\.\.\.$/, '') : 'Evaluating condition';
156
+ cliLogger.planning(`[🤖 ${agent.name}] ${displayText}`);
157
+
158
+ // Use call_llm action to evaluate condition with LLM
159
+ const callLlmAction = (await import('./call-llm.js')).default;
160
+
161
+ // Resolve data references
162
+ const resolvedData = agent.resolveObjectReferences(condition.data || {}, context);
163
+
164
+ // Create call_llm action to evaluate condition
165
+ const callLlmRequest = {
166
+ data: resolvedData,
167
+ instruction: condition.instruction + "\n\nRESPOND WITH ONLY 'true' or 'false' (lowercase, no quotes, no explanation)."
168
+ };
169
+
170
+ const llmResult = await callLlmAction.execute(callLlmRequest, agent);
171
+ const resultText = llmResult.result.trim().toLowerCase();
172
+
173
+ // Clear progress
174
+ cliLogger.clear();
175
+
176
+ // Parse boolean result
177
+ conditionResult = resultText === 'true';
178
+ } else {
179
+ // Simple string condition - evaluate with JavaScript
180
+ conditionResult = agent.evaluateCondition(condition, context);
181
+ }
182
+ } catch (error) {
183
+ throw new Error(`Failed to evaluate while condition: ${error.message}`);
184
+ }
185
+
186
+ // If condition is false, stop (don't continue to next iteration)
187
+ if (!conditionResult) {
188
+ stoppedReason = 'condition_false';
189
+ break;
190
+ }
191
+
192
+ // Safety check
193
+ if (iterations >= maxIterations) {
194
+ stoppedReason = 'max_iterations';
195
+ break;
196
+ }
197
+ }
198
+
199
+ return {
200
+ iterations,
201
+ results: allResults,
202
+ stopped_reason: stoppedReason
203
+ };
204
+ }
205
+ };