@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
@@ -1,22 +1,22 @@
1
1
  /**
2
- * Format Action - Use LLM to transform/format data dynamically
2
+ * Call LLM Action - Call an LLM to process data dynamically at runtime
3
3
  */
4
4
 
5
5
  export default {
6
- type: 'format',
7
- intent: 'format',
8
- description: 'Use LLM to format/transform data according to instructions → Returns: { formatted: "result text" }. Access with ${id.output.formatted}. IMPORTANT: format action MUST have an "id" to save the result!',
6
+ type: 'call_llm',
7
+ intent: 'call_llm',
8
+ description: 'Call an LLM to process data based on instructions → Returns: { result: "processed text" }. Access with ${id.output.result}. IMPORTANT: call_llm action MUST have an "id" to save the result!',
9
9
  permission: 'execute',
10
10
 
11
11
  schema: {
12
12
  type: 'object',
13
13
  properties: {
14
14
  data: {
15
- description: 'Data to format (any type: object, array, string, etc.)'
15
+ description: 'Data to process (any type: object, array, string, etc.)'
16
16
  },
17
17
  instruction: {
18
18
  type: 'string',
19
- description: 'Natural language instruction describing how to format the data'
19
+ description: 'Natural language instruction describing what to do with the data'
20
20
  }
21
21
  },
22
22
  required: ['data', 'instruction']
@@ -24,7 +24,7 @@ export default {
24
24
 
25
25
  examples: [
26
26
  {
27
- type: 'format',
27
+ type: 'call_llm',
28
28
  data: '${previousResult.users}',
29
29
  instruction: 'Generate a markdown table with columns: Sr/Sra (deduce from name), Name, Age'
30
30
  }
@@ -34,37 +34,23 @@ export default {
34
34
  const { data, instruction } = action;
35
35
 
36
36
  if (!instruction) {
37
- throw new Error('Format action requires an instruction');
37
+ throw new Error('call_llm action requires an instruction');
38
38
  }
39
39
 
40
40
  if (!agent.llmProvider) {
41
41
  throw new Error('Agent does not have an LLM provider configured');
42
42
  }
43
43
 
44
- if (process.env.KOI_DEBUG_LLM) {
45
- console.error(`[Agent] 🎨 Formatting data with LLM (instruction: "${instruction.substring(0, 50)}...")`);
46
- console.error(`[Agent] 📊 Data received:`, JSON.stringify(data, null, 2));
47
- }
48
-
49
- try {
50
- // Call OpenAI directly for a simple formatting task
51
- const completion = await agent.llmProvider.openai.chat.completions.create({
52
- model: 'gpt-4o-mini',
53
- temperature: 0.3,
54
- max_tokens: 2000,
55
- messages: [
56
- {
57
- role: 'system',
58
- content: `You are a data formatter. Your job is to transform data according to user instructions.
44
+ const systemPrompt = `You are a data processor. Your job is to process data according to user instructions.
59
45
 
60
46
  CRITICAL RULES:
61
- 1. Return ONLY the formatted output - NO explanations, NO markdown wrapping, NO code blocks, NO JSON
47
+ 1. Return ONLY the processed result - NO explanations, NO markdown wrapping, NO code blocks, NO JSON wrapper
62
48
  2. Follow the instruction exactly as specified
63
49
  3. NEVER generate template variables (\${...}) or placeholders ([name], {x}, [DD]) - use ACTUAL VALUES from data
64
50
  4. When calculations are needed (dates, time differences, derived values), perform them accurately
65
51
  5. Use the most authoritative data source available (e.g., birthdate over age field, timestamps over derived dates)
66
52
  6. Current date for any time-based calculations: ${new Date().toISOString().split('T')[0]}
67
- 7. If instruction says "generate emails", "generate text", "format as", output formatted TEXT - NOT JSON or arrays
53
+ 7. If instruction says "generate", "format as", output the result as TEXT - NOT JSON or arrays
68
54
  8. Default output should be human-readable text unless instruction explicitly asks for JSON/table/specific format
69
55
 
70
56
  CALCULATION REQUIREMENTS:
@@ -74,16 +60,15 @@ CALCULATION REQUIREMENTS:
74
60
  - Use birthdate field as authoritative source, ignore any "age" field as it may be stale
75
61
  - Verify results make logical sense (e.g., age should be positive and reasonable)
76
62
 
77
- EMAIL/TEXT GENERATION:
63
+ CONTENT GENERATION:
78
64
  - When generating emails or personalized text, create properly formatted text for each item
79
65
  - Include salutations, body text, and sign-offs as appropriate
80
66
  - Separate multiple emails/items with blank lines
81
67
  - Use natural, human-friendly language
82
- - Infer formatting details from context (e.g., "Estimado" vs "Estimada" based on names ending in 'a')`
83
- },
84
- {
85
- role: 'user',
86
- content: `IMPORTANT: Today's date is ${new Date().toISOString().split('T')[0]}. Use this for all date calculations.
68
+ - Infer formatting details from context (e.g., "Estimado" vs "Estimada" based on names ending in 'a')
69
+ - Generate DIFFERENT content each time based on the data - don't reuse the same text for different values`;
70
+
71
+ const userPrompt = `IMPORTANT: Today's date is ${new Date().toISOString().split('T')[0]}. Use this for all date calculations.
87
72
 
88
73
  Data:
89
74
  ${JSON.stringify(data, null, 2)}
@@ -91,24 +76,32 @@ ${JSON.stringify(data, null, 2)}
91
76
  Instruction:
92
77
  ${instruction}
93
78
 
94
- Output (formatted result only):`
95
- }
96
- ]
97
- });
79
+ Output (result only):`;
98
80
 
99
- let formattedText = completion.choices[0].message.content.trim();
81
+ try {
82
+ // Call OpenAI with centralized logging
83
+ const completion = await agent.llmProvider.callOpenAI(
84
+ {
85
+ model: 'gpt-4o-mini',
86
+ temperature: 0.3,
87
+ max_tokens: 2000,
88
+ messages: [
89
+ { role: 'system', content: systemPrompt },
90
+ { role: 'user', content: userPrompt }
91
+ ]
92
+ },
93
+ 'call_llm action'
94
+ );
95
+
96
+ let resultText = completion.choices[0].message.content.trim();
100
97
 
101
98
  // Clean up any markdown code blocks that might have leaked through
102
- formattedText = formattedText.replace(/^```[\w]*\n/gm, '').replace(/\n```$/gm, '');
103
- formattedText = formattedText.trim();
104
-
105
- if (process.env.KOI_DEBUG_LLM) {
106
- console.error(`[Agent] ✅ Formatted ${formattedText.length} characters`);
107
- }
99
+ resultText = resultText.replace(/^```[\w]*\n/gm, '').replace(/\n```$/gm, '');
100
+ resultText = resultText.trim();
108
101
 
109
- return { formatted: formattedText };
102
+ return { result: resultText };
110
103
  } catch (error) {
111
- console.error(`[Agent] ❌ Format action failed: ${error.message}`);
104
+ agent.llmProvider.logError('call_llm action failed', error);
112
105
  throw error;
113
106
  }
114
107
  }
@@ -0,0 +1,97 @@
1
+ /**
2
+ * call_mcp action - Call a tool on an MCP (Model Context Protocol) server.
3
+ *
4
+ * Used by the LLM to invoke external tools exposed via MCP stdio servers.
5
+ */
6
+ export default {
7
+ type: 'call_mcp',
8
+ intent: 'call_mcp',
9
+ description: 'Call a tool on an MCP server. Requires: mcp (server name), tool (tool name), input (parameters object)',
10
+ permission: 'execute',
11
+ hidden: false,
12
+
13
+ schema: {
14
+ type: 'object',
15
+ properties: {
16
+ mcp: { type: 'string', description: 'MCP server name' },
17
+ tool: { type: 'string', description: 'Tool name to invoke' },
18
+ input: { type: 'object', description: 'Tool input parameters' }
19
+ },
20
+ required: ['mcp', 'tool']
21
+ },
22
+
23
+ examples: [
24
+ {
25
+ actionType: 'direct',
26
+ intent: 'call_mcp',
27
+ mcp: 'mobileMCP',
28
+ tool: 'tap',
29
+ input: { x: 100, y: 200 }
30
+ },
31
+ {
32
+ actionType: 'direct',
33
+ intent: 'call_mcp',
34
+ mcp: 'mobileMCP',
35
+ tool: 'screenshot',
36
+ input: {}
37
+ }
38
+ ],
39
+
40
+ async execute(action, agent) {
41
+ const { mcp, tool, input = {} } = action;
42
+
43
+ if (!mcp) {
44
+ throw new Error('call_mcp: "mcp" field is required (MCP server name)');
45
+ }
46
+ if (!tool) {
47
+ throw new Error('call_mcp: "tool" field is required (tool name)');
48
+ }
49
+
50
+ // Verify agent has access to this MCP
51
+ if (agent.usesMCPNames && agent.usesMCPNames.length > 0 && !agent.usesMCPNames.includes(mcp)) {
52
+ throw new Error(`Agent ${agent.name} does not have access to MCP: ${mcp}. Available: ${agent.usesMCPNames.join(', ')}`);
53
+ }
54
+
55
+ // Get the global mcpRegistry
56
+ const mcpRegistry = globalThis.mcpRegistry;
57
+ if (!mcpRegistry) {
58
+ throw new Error('call_mcp: MCP registry not available. Make sure MCP servers are declared in your .koi file.');
59
+ }
60
+
61
+ // Check MCP server status — auto-reconnect if it crashed
62
+ const client = mcpRegistry.get(mcp);
63
+ if (!client) {
64
+ throw new Error(`MCP server "${mcp}" is not registered. Check your .koi file.`);
65
+ }
66
+ if (!client.initialized) {
67
+ if (process.env.KOI_DEBUG_LLM) {
68
+ console.error(`[call_mcp] MCP "${mcp}" is down, reconnecting...`);
69
+ }
70
+ await client.connect();
71
+ }
72
+
73
+ if (process.env.KOI_DEBUG_LLM) {
74
+ console.error(`[call_mcp] ${mcp}.${tool}(${JSON.stringify(input).substring(0, 200)})`);
75
+ }
76
+
77
+ const result = await mcpRegistry.callTool(mcp, tool, input);
78
+
79
+ if (process.env.KOI_DEBUG_LLM) {
80
+ console.error(`[call_mcp] Result: ${JSON.stringify(result).substring(0, 200)}`);
81
+ }
82
+
83
+ // When the MCP tool returns an error, attach recent stderr output
84
+ // so the LLM can see actual error details (e.g. installation commands).
85
+ // MCP servers often print detailed instructions to stderr but only return
86
+ // a summary message in the result payload.
87
+ if (result && result.success === false && client._stderrLines?.length > 0) {
88
+ const stderrContext = client._stderrLines.join('\n');
89
+ result.serverOutput = stderrContext;
90
+ if (process.env.KOI_DEBUG_LLM) {
91
+ console.error(`[call_mcp] Attached ${client._stderrLines.length} stderr lines to error result`);
92
+ }
93
+ }
94
+
95
+ return result;
96
+ }
97
+ };
@@ -0,0 +1,179 @@
1
+ /**
2
+ * If Action - Conditional execution
3
+ */
4
+ import { buildActionDisplay } from '../cli-display.js';
5
+
6
+ export default {
7
+ type: 'if',
8
+ intent: 'if',
9
+ description: 'Execute actions conditionally. CONDITION: Use string "${a1.output} === \'yes\'" for exact match, OR object with llm_eval for semantic: { "llm_eval": true, "instruction": "Return true if user agrees", "data": "${a1.output.answer}" } → Returns: { executed: "then"|"else", result: ... }',
10
+ permission: 'execute',
11
+
12
+ schema: {
13
+ type: 'object',
14
+ properties: {
15
+ condition: {
16
+ type: 'string',
17
+ description: 'Condition to evaluate (e.g., "${a1.output.answer} === \'Yes\'")'
18
+ },
19
+ then: {
20
+ type: 'array',
21
+ description: 'Actions to execute if condition is true'
22
+ },
23
+ else: {
24
+ type: 'array',
25
+ description: 'Actions to execute if condition is false (optional)'
26
+ }
27
+ },
28
+ required: ['condition', 'then']
29
+ },
30
+
31
+ examples: [
32
+ {
33
+ id: 'a1',
34
+ intent: 'prompt_user',
35
+ question: 'Do you want to continue?',
36
+ options: ['Yes', 'No']
37
+ },
38
+ {
39
+ intent: 'if',
40
+ condition: "${a1.output.answer} === 'Yes'",
41
+ then: [
42
+ { id: 'a2', intent: 'prompt_user', question: 'What is your age?' },
43
+ { intent: 'print', message: 'Your age is: ${a2.output.answer}' }
44
+ ],
45
+ else: [
46
+ { intent: 'print', message: 'Goodbye!' }
47
+ ]
48
+ }
49
+ ],
50
+
51
+ // Executor function
52
+ async execute(action, agent) {
53
+ const condition = action.condition || action.data?.condition || '';
54
+ const thenActions = action.then || action.data?.then || [];
55
+ const elseActions = action.else || action.data?.else || [];
56
+
57
+ if (!condition) {
58
+ throw new Error('if action requires a "condition" field');
59
+ }
60
+
61
+ // Get the current action context from the agent
62
+ const context = agent._currentActionContext || {
63
+ state: agent.state,
64
+ results: []
65
+ };
66
+
67
+ // Evaluate condition
68
+ let conditionResult = false;
69
+ try {
70
+ // Check if condition is LLM-evaluated (object with llm_eval: true)
71
+ if (typeof condition === 'object' && condition.llm_eval === true) {
72
+ // Show progress while evaluating condition
73
+ const displayText = condition.desc ? condition.desc.replace(/\.\.\.$/, '') : 'Evaluating condition';
74
+ cliLogger.planning(`[🤖 ${agent.name}] ${displayText}`);
75
+
76
+ // Use call_llm action to evaluate condition with LLM
77
+ const callLlmAction = (await import('./call-llm.js')).default;
78
+
79
+ // Resolve data references
80
+ const resolvedData = agent.resolveObjectReferences(condition.data || {}, context);
81
+
82
+ // Create call_llm action to evaluate condition
83
+ const callLlmRequest = {
84
+ data: resolvedData,
85
+ instruction: condition.instruction + "\n\nRESPOND WITH ONLY 'true' or 'false' (lowercase, no quotes, no explanation)."
86
+ };
87
+
88
+ const llmResult = await callLlmAction.execute(callLlmRequest, agent);
89
+ const resultText = llmResult.result.trim().toLowerCase();
90
+
91
+ // Clear progress
92
+ cliLogger.clear();
93
+
94
+ // Parse boolean result
95
+ conditionResult = resultText === 'true';
96
+ } else {
97
+ // Simple string condition - evaluate with JavaScript
98
+ conditionResult = agent.evaluateCondition(condition, context);
99
+ }
100
+ } catch (error) {
101
+ throw new Error(`Failed to evaluate condition: ${error.message}`);
102
+ }
103
+
104
+ // Execute appropriate branch
105
+ const actionsToExecute = conditionResult ? thenActions : elseActions;
106
+
107
+ if (!actionsToExecute || actionsToExecute.length === 0) {
108
+ return { executed: conditionResult ? 'then' : 'else', result: null };
109
+ }
110
+
111
+ // Execute actions with inherited context (so nested actions can access a1, a2, etc.)
112
+ // We need to execute them manually to preserve the parent context
113
+ const actionRegistry = (await import('../action-registry.js')).actionRegistry;
114
+ const cliLogger = (await import('../cli-logger.js')).cliLogger;
115
+
116
+ let result = null;
117
+ for (const nestedAction of actionsToExecute) {
118
+ // Resolve references using the parent context
119
+ const resolvedAction = agent.resolveActionReferences(nestedAction, context);
120
+
121
+ // Show progress
122
+ cliLogger.planning(buildActionDisplay(agent.name, resolvedAction));
123
+
124
+ // Check if this is a delegation action
125
+ if (resolvedAction.actionType === 'delegate') {
126
+ // Delegation: route to appropriate team member
127
+ result = await agent.resolveAction(resolvedAction, context);
128
+ } else {
129
+ // Direct action: Get action definition from registry
130
+ const actionDef = actionRegistry.get(nestedAction.intent || nestedAction.type);
131
+
132
+ if (actionDef && actionDef.execute) {
133
+ // Update agent's current context for nested action
134
+ const previousContext = agent._currentActionContext;
135
+ agent._currentActionContext = context;
136
+
137
+ // Execute with agent
138
+ result = await actionDef.execute(resolvedAction, agent);
139
+
140
+ // Restore previous context
141
+ agent._currentActionContext = previousContext;
142
+ } else {
143
+ cliLogger.clear();
144
+ throw new Error(`Action ${nestedAction.intent || nestedAction.type} not found`);
145
+ }
146
+ }
147
+
148
+ cliLogger.clear();
149
+
150
+ // Update parent context with result
151
+ if (result && typeof result === 'object') {
152
+ // Unwrap double-encoded results (LLM sometimes returns { "result": "{...json...}" })
153
+ if (result.result && typeof result.result === 'string' && Object.keys(result).length === 1) {
154
+ try {
155
+ const parsed = JSON.parse(result.result);
156
+ if (typeof parsed === 'object') {
157
+ result = parsed;
158
+ }
159
+ } catch (e) {
160
+ // Not JSON, keep as-is
161
+ }
162
+ }
163
+
164
+ const resultForContext = JSON.parse(JSON.stringify(result));
165
+ context.results.push(resultForContext);
166
+
167
+ // Store with action ID if provided
168
+ if (nestedAction.id) {
169
+ context[nestedAction.id] = { output: resultForContext };
170
+ }
171
+ }
172
+ }
173
+
174
+ return {
175
+ executed: conditionResult ? 'then' : 'else',
176
+ result: result
177
+ };
178
+ }
179
+ };
@@ -3,6 +3,7 @@
3
3
  */
4
4
 
5
5
  import { cliLogger } from '../cli-logger.js';
6
+ import { renderMarkdown } from '../cli-markdown.js';
6
7
 
7
8
  export default {
8
9
  type: 'print', // Mantener temporalmente
@@ -34,8 +35,9 @@ export default {
34
35
  const message = action.message || action.text || action.data || '';
35
36
 
36
37
  // Print directly to stdout (bypassing cliLogger interception)
38
+ // Reset any leaked ANSI styles, then print plain white (no bold)
37
39
  cliLogger.clearProgress();
38
- process.stdout.write(message + '\n');
40
+ process.stdout.write(`\x1b[0m${renderMarkdown(message)}\n`);
39
41
 
40
42
  return { printed: true, message };
41
43
  }
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Prompt User Action - Ask user for input via command line
3
+ */
4
+
5
+ import { cliLogger } from '../cli-logger.js';
6
+ import { cliSelect } from '../cli-select.js';
7
+ import { cliInput } from '../cli-input.js';
8
+
9
+ export default {
10
+ type: 'prompt_user',
11
+ intent: 'prompt_user',
12
+ description: 'Ask the user a question via command line. Can include "options" array for interactive menu when user must choose from a limited set (e.g., ["Sí", "No"], ["Opción A", "Opción B", "Opción C"]) - useful for Yes/No, multiple choice, etc. CRITICAL: "question" field ONLY accepts 100% static text OR ${variable} reference to call_llm result. If question needs generation/adaptation (keywords: random, relacionado, based on, adapted), you MUST use call_llm FIRST to generate it, then use ${result} here → Returns: { answer }. Access with ${id.output.answer}',
13
+ permission: 'execute', // Requires execute permission
14
+
15
+ schema: {
16
+ type: 'object',
17
+ properties: {
18
+ question: {
19
+ type: 'string',
20
+ description: 'The question to ask the user'
21
+ },
22
+ options: {
23
+ type: 'array',
24
+ description: 'Optional array of choices for interactive menu (e.g., ["Yes", "No"]). User navigates with arrows and selects with Enter.'
25
+ },
26
+ prompt: {
27
+ type: 'string',
28
+ description: 'Optional custom prompt for text input mode (defaults to "> ")'
29
+ }
30
+ },
31
+ required: ['question']
32
+ },
33
+
34
+ examples: [
35
+ { id: 'a1', intent: 'prompt_user', question: 'What is your name?' },
36
+ { intent: 'print', message: 'Hello ${a1.output.answer}!' },
37
+ { id: 'a2', intent: 'prompt_user', question: 'Do you want to proceed?', options: ['Yes', 'No'] },
38
+ { intent: 'print', message: 'You selected: ${a2.output.answer}' }
39
+ ],
40
+
41
+ // Executor function - receives the action and agent context
42
+ async execute(action, agent) {
43
+ const question = action.question || action.data?.question || '';
44
+ const options = action.options || action.data?.options || null;
45
+ const promptText = action.prompt || action.data?.prompt || '> ';
46
+
47
+ if (!question) {
48
+ throw new Error('prompt_user action requires a "question" field');
49
+ }
50
+
51
+ // Clear any progress indicators
52
+ cliLogger.clearProgress();
53
+
54
+ // If options are provided, show interactive menu
55
+ if (options && Array.isArray(options) && options.length > 0) {
56
+ const value = await cliSelect(question, options.map((opt) => ({
57
+ title: opt,
58
+ value: opt
59
+ })));
60
+
61
+ // Return the selected option
62
+ return { answer: value || options[0] };
63
+ }
64
+
65
+ // Text input mode with rich line editor
66
+ // Print the question in plain white (agent talking)
67
+ process.stdout.write(`\n\x1b[0m${question}\n`);
68
+
69
+ // Show rich input with block cursor
70
+ const answer = await cliInput(promptText);
71
+
72
+ // Return the user's answer
73
+ return { answer };
74
+ }
75
+ };
@@ -0,0 +1,147 @@
1
+ /**
2
+ * Repeat Action - Execute actions N times
3
+ */
4
+ import { buildActionDisplay } from '../cli-display.js';
5
+
6
+ export default {
7
+ type: 'repeat',
8
+ intent: 'repeat',
9
+ description: 'Execute actions a fixed number of times. Use for "ask 3 times", "do X times" → Returns: { iterations: N, results: [array of results] }',
10
+ permission: 'execute',
11
+
12
+ schema: {
13
+ type: 'object',
14
+ properties: {
15
+ count: {
16
+ type: 'number',
17
+ description: 'Number of times to repeat (e.g., 3)'
18
+ },
19
+ actions: {
20
+ type: 'array',
21
+ description: 'Actions to execute in each iteration'
22
+ }
23
+ },
24
+ required: ['count', 'actions']
25
+ },
26
+
27
+ examples: [
28
+ {
29
+ intent: 'repeat',
30
+ count: 3,
31
+ actions: [
32
+ { id: 'a1', intent: 'prompt_user', question: '¿Cuántos años tienes?' },
33
+ { intent: 'print', message: 'Respuesta ${iteration}: ${a1.output.answer}' }
34
+ ]
35
+ }
36
+ ],
37
+
38
+ // Executor function
39
+ async execute(action, agent) {
40
+ const count = action.count || action.data?.count || 1;
41
+ const actions = action.actions || action.data?.actions || [];
42
+
43
+ if (!actions || actions.length === 0) {
44
+ throw new Error('repeat action requires an "actions" array');
45
+ }
46
+
47
+ if (typeof count !== 'number' || count < 1) {
48
+ throw new Error('repeat action requires a positive number for "count"');
49
+ }
50
+
51
+ // Get the current action context from the agent
52
+ const context = agent._currentActionContext || {
53
+ state: agent.state,
54
+ results: []
55
+ };
56
+
57
+ // Execute actions with inherited context
58
+ const actionRegistry = (await import('../action-registry.js')).actionRegistry;
59
+ const cliLogger = (await import('../cli-logger.js')).cliLogger;
60
+
61
+ const allResults = [];
62
+
63
+ for (let i = 0; i < count; i++) {
64
+ // Create iteration context with current iteration number
65
+ const iterationContext = {
66
+ ...context,
67
+ iteration: i + 1, // 1-based for user-friendly display
68
+ iterationIndex: i // 0-based for array access
69
+ };
70
+
71
+ for (const nestedAction of actions) {
72
+ // Resolve references using the iteration context
73
+ const resolvedAction = agent.resolveActionReferences(nestedAction, iterationContext);
74
+
75
+ // Show progress
76
+ cliLogger.planning(buildActionDisplay(agent.name, resolvedAction));
77
+
78
+ let result;
79
+
80
+ // Check if this is a delegation action
81
+ if (resolvedAction.actionType === 'delegate') {
82
+ // Delegation: route to appropriate team member
83
+ result = await agent.resolveAction(resolvedAction, iterationContext);
84
+ } else {
85
+ // Direct action: Get action definition from registry
86
+ const actionDef = actionRegistry.get(nestedAction.intent || nestedAction.type);
87
+
88
+ if (actionDef && actionDef.execute) {
89
+ // Update agent's current context for nested action
90
+ const previousContext = agent._currentActionContext;
91
+ agent._currentActionContext = iterationContext;
92
+
93
+ // Execute with agent
94
+ result = await actionDef.execute(resolvedAction, agent);
95
+
96
+ // Restore previous context
97
+ agent._currentActionContext = previousContext;
98
+ } else {
99
+ cliLogger.clear();
100
+ throw new Error(`Action ${nestedAction.intent || nestedAction.type} not found`);
101
+ }
102
+ }
103
+
104
+ cliLogger.clear();
105
+
106
+ // Update iteration context with result
107
+ if (result && typeof result === 'object') {
108
+ // Unwrap double-encoded results (LLM sometimes returns { "result": "{...json...}" })
109
+ if (result.result && typeof result.result === 'string' && Object.keys(result).length === 1) {
110
+ try {
111
+ const parsed = JSON.parse(result.result);
112
+ if (typeof parsed === 'object') {
113
+ result = parsed;
114
+ }
115
+ } catch (e) {
116
+ // Not JSON, keep as-is
117
+ }
118
+ }
119
+
120
+ const resultForContext = JSON.parse(JSON.stringify(result));
121
+ iterationContext.results.push(resultForContext);
122
+
123
+ // Store with action ID if provided
124
+ if (nestedAction.id) {
125
+ iterationContext[nestedAction.id] = { output: resultForContext };
126
+
127
+ // CRITICAL: Propagate action ID results back to parent context
128
+ // so template variables like ${right_turn.output.answer} work outside the repeat
129
+ context[nestedAction.id] = { output: resultForContext };
130
+
131
+ if (process.env.KOI_DEBUG_LLM) {
132
+ console.error(`[repeat] Stored result for ID "${nestedAction.id}":`, JSON.stringify(resultForContext));
133
+ }
134
+ }
135
+
136
+ // Track all results
137
+ allResults.push(resultForContext);
138
+ }
139
+ }
140
+ }
141
+
142
+ return {
143
+ iterations: count,
144
+ results: allResults
145
+ };
146
+ }
147
+ };