@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,22 +1,22 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Call LLM Action - Call an LLM to process data dynamically at runtime
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
export default {
|
|
6
|
-
type: '
|
|
7
|
-
intent: '
|
|
8
|
-
description: '
|
|
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
|
|
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
|
|
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: '
|
|
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('
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
95
|
-
}
|
|
96
|
-
]
|
|
97
|
-
});
|
|
79
|
+
Output (result only):`;
|
|
98
80
|
|
|
99
|
-
|
|
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
|
-
|
|
103
|
-
|
|
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 {
|
|
102
|
+
return { result: resultText };
|
|
110
103
|
} catch (error) {
|
|
111
|
-
|
|
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
|
|
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
|
+
};
|