@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.
- package/QUICKSTART.md +89 -0
- package/README.md +545 -0
- package/examples/actions-demo.koi +177 -0
- package/examples/cache-test.koi +29 -0
- package/examples/calculator.koi +61 -0
- package/examples/clear-registry.js +33 -0
- package/examples/clear-registry.koi +30 -0
- package/examples/code-introspection-test.koi +149 -0
- package/examples/counter.koi +132 -0
- package/examples/delegation-test.koi +52 -0
- package/examples/directory-import-test.koi +84 -0
- package/examples/hello-world-claude.koi +52 -0
- package/examples/hello-world.koi +52 -0
- package/examples/hello.koi +24 -0
- package/examples/mcp-example.koi +70 -0
- package/examples/multi-event-handler-test.koi +144 -0
- package/examples/new-import-test.koi +89 -0
- package/examples/pipeline.koi +162 -0
- package/examples/registry-demo.koi +184 -0
- package/examples/registry-playbook-demo.koi +162 -0
- package/examples/registry-playbook-email-compositor-2.koi +140 -0
- package/examples/registry-playbook-email-compositor.koi +140 -0
- package/examples/sentiment.koi +90 -0
- package/examples/simple.koi +48 -0
- package/examples/skill-import-test.koi +76 -0
- package/examples/skills/advanced/index.koi +95 -0
- package/examples/skills/math-operations.koi +69 -0
- package/examples/skills/string-operations.koi +56 -0
- package/examples/task-chaining-demo.koi +244 -0
- package/examples/test-await.koi +22 -0
- package/examples/test-crypto-sha256.koi +196 -0
- package/examples/test-delegation.koi +41 -0
- package/examples/test-multi-team-routing.koi +258 -0
- package/examples/test-no-handler.koi +35 -0
- package/examples/test-npm-import.koi +67 -0
- package/examples/test-parse.koi +10 -0
- package/examples/test-peers-with-team.koi +59 -0
- package/examples/test-permissions-fail.koi +20 -0
- package/examples/test-permissions.koi +36 -0
- package/examples/test-simple-registry.koi +31 -0
- package/examples/test-typescript-import.koi +64 -0
- package/examples/test-uses-team-syntax.koi +25 -0
- package/examples/test-uses-team.koi +31 -0
- package/examples/utils/calculator.test.ts +144 -0
- package/examples/utils/calculator.ts +56 -0
- package/examples/utils/math-helpers.js +50 -0
- package/examples/utils/math-helpers.ts +55 -0
- package/examples/web-delegation-demo.koi +165 -0
- package/package.json +78 -0
- package/src/cli/koi.js +793 -0
- package/src/compiler/build-optimizer.js +447 -0
- package/src/compiler/cache-manager.js +274 -0
- package/src/compiler/import-resolver.js +369 -0
- package/src/compiler/parser.js +7542 -0
- package/src/compiler/transpiler.js +1105 -0
- package/src/compiler/typescript-transpiler.js +148 -0
- package/src/grammar/koi.pegjs +767 -0
- package/src/runtime/action-registry.js +172 -0
- package/src/runtime/actions/call-skill.js +45 -0
- package/src/runtime/actions/format.js +115 -0
- package/src/runtime/actions/print.js +42 -0
- package/src/runtime/actions/registry-delete.js +37 -0
- package/src/runtime/actions/registry-get.js +37 -0
- package/src/runtime/actions/registry-keys.js +33 -0
- package/src/runtime/actions/registry-search.js +34 -0
- package/src/runtime/actions/registry-set.js +50 -0
- package/src/runtime/actions/return.js +31 -0
- package/src/runtime/actions/send-message.js +58 -0
- package/src/runtime/actions/update-state.js +36 -0
- package/src/runtime/agent.js +1368 -0
- package/src/runtime/cli-logger.js +205 -0
- package/src/runtime/incremental-json-parser.js +201 -0
- package/src/runtime/index.js +33 -0
- package/src/runtime/llm-provider.js +1372 -0
- package/src/runtime/mcp-client.js +1171 -0
- package/src/runtime/planner.js +273 -0
- package/src/runtime/registry-backends/keyv-sqlite.js +215 -0
- package/src/runtime/registry-backends/local.js +260 -0
- package/src/runtime/registry.js +162 -0
- package/src/runtime/role.js +14 -0
- package/src/runtime/router.js +395 -0
- package/src/runtime/runtime.js +113 -0
- package/src/runtime/skill-selector.js +173 -0
- package/src/runtime/skill.js +25 -0
- package/src/runtime/team.js +162 -0
|
@@ -0,0 +1,1372 @@
|
|
|
1
|
+
import OpenAI from 'openai';
|
|
2
|
+
import Anthropic from '@anthropic-ai/sdk';
|
|
3
|
+
import dotenv from 'dotenv';
|
|
4
|
+
import { cliLogger } from './cli-logger.js';
|
|
5
|
+
import { actionRegistry } from './action-registry.js';
|
|
6
|
+
import { IncrementalJSONParser } from './incremental-json-parser.js';
|
|
7
|
+
|
|
8
|
+
// Load .env file but don't override existing environment variables
|
|
9
|
+
// Silent by default - dotenv will not log unless there's an error
|
|
10
|
+
const originalWrite = process.stdout.write;
|
|
11
|
+
process.stdout.write = () => {}; // Temporarily silence stdout
|
|
12
|
+
dotenv.config({ override: false });
|
|
13
|
+
process.stdout.write = originalWrite; // Restore stdout
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Format prompt text with > prefix for each line (for debug output)
|
|
17
|
+
*/
|
|
18
|
+
function formatPromptForDebug(text) {
|
|
19
|
+
return text.split('\n').map(line => `> \x1b[90m${line}\x1b[0m`).join('\n');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export class LLMProvider {
|
|
23
|
+
constructor(config = {}) {
|
|
24
|
+
this.provider = config.provider || 'openai';
|
|
25
|
+
this.model = config.model || 'gpt-4o-mini';
|
|
26
|
+
this.temperature = config.temperature ?? 0.1; // Low temperature for deterministic results
|
|
27
|
+
this.maxTokens = config.max_tokens || 8000; // Increased to avoid truncation of long responses
|
|
28
|
+
|
|
29
|
+
// Initialize clients
|
|
30
|
+
if (this.provider === 'openai') {
|
|
31
|
+
const apiKey = process.env.OPENAI_API_KEY;
|
|
32
|
+
if (!apiKey) {
|
|
33
|
+
console.error('\n⚠️ OPENAI_API_KEY not found!');
|
|
34
|
+
console.error(' Set it as environment variable or create a .env file\n');
|
|
35
|
+
throw new Error('OPENAI_API_KEY is required for OpenAI provider');
|
|
36
|
+
}
|
|
37
|
+
this.openai = new OpenAI({ apiKey });
|
|
38
|
+
} else if (this.provider === 'anthropic') {
|
|
39
|
+
const apiKey = process.env.ANTHROPIC_API_KEY;
|
|
40
|
+
if (!apiKey) {
|
|
41
|
+
console.error('\n⚠️ ANTHROPIC_API_KEY not found!');
|
|
42
|
+
console.error(' Set it as environment variable or create a .env file\n');
|
|
43
|
+
throw new Error('ANTHROPIC_API_KEY is required for Anthropic provider');
|
|
44
|
+
}
|
|
45
|
+
this.anthropic = new Anthropic({ apiKey });
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async executePlanning(prompt) {
|
|
50
|
+
// Simple, fast planning call without all the overhead
|
|
51
|
+
// ALWAYS use the fastest model for planning
|
|
52
|
+
try {
|
|
53
|
+
let response;
|
|
54
|
+
|
|
55
|
+
if (this.provider === 'openai') {
|
|
56
|
+
const completion = await this.openai.chat.completions.create({
|
|
57
|
+
model: 'gpt-4o-mini', // Force fastest model for planning
|
|
58
|
+
messages: [
|
|
59
|
+
{
|
|
60
|
+
role: 'system',
|
|
61
|
+
content: 'Planning assistant. JSON only.'
|
|
62
|
+
},
|
|
63
|
+
{ role: 'user', content: prompt }
|
|
64
|
+
],
|
|
65
|
+
temperature: 0, // Use 0 for maximum determinism
|
|
66
|
+
max_tokens: 800
|
|
67
|
+
});
|
|
68
|
+
response = completion.choices[0].message.content.trim();
|
|
69
|
+
} else if (this.provider === 'anthropic') {
|
|
70
|
+
const completion = await this.anthropic.messages.create({
|
|
71
|
+
model: 'claude-3-haiku-20240307', // Force fastest Anthropic model
|
|
72
|
+
max_tokens: 800,
|
|
73
|
+
temperature: 0, // Use 0 for maximum determinism
|
|
74
|
+
messages: [{ role: 'user', content: prompt }],
|
|
75
|
+
system: 'Planning assistant. JSON only.'
|
|
76
|
+
});
|
|
77
|
+
response = completion.content[0].text.trim();
|
|
78
|
+
} else {
|
|
79
|
+
throw new Error(`Unknown provider: ${this.provider}`);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Parse JSON
|
|
83
|
+
return JSON.parse(response);
|
|
84
|
+
} catch (error) {
|
|
85
|
+
throw new Error(`Planning failed: ${error.message}`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async executePlaybook(playbook, context = {}, agentName = null, tools = [], agent = null, fromDelegation = false, onAction = null) {
|
|
90
|
+
// Show planning animation while LLM is thinking
|
|
91
|
+
// Format: [🤖 AgentName] Thinking...
|
|
92
|
+
const planningPrefix = agentName ? `[🤖 ${agentName}]` : '';
|
|
93
|
+
cliLogger.planning(`${planningPrefix} Thinking`);
|
|
94
|
+
|
|
95
|
+
// Build prompt with context - but keep it minimal
|
|
96
|
+
const contextStr = Object.keys(context).length > 0
|
|
97
|
+
? `\n\nContext: ${JSON.stringify(context)}\n`
|
|
98
|
+
: '';
|
|
99
|
+
|
|
100
|
+
const prompt = `${playbook}${contextStr}
|
|
101
|
+
|
|
102
|
+
Respond with ONLY valid JSON.`;
|
|
103
|
+
|
|
104
|
+
try {
|
|
105
|
+
let response;
|
|
106
|
+
|
|
107
|
+
// Use streaming if onAction callback is provided AND no tools
|
|
108
|
+
// (Tools currently don't support streaming)
|
|
109
|
+
const useStreaming = onAction && (!tools || tools.length === 0);
|
|
110
|
+
|
|
111
|
+
if (this.provider === 'openai') {
|
|
112
|
+
if (useStreaming) {
|
|
113
|
+
// hasTeams should only be true if agent can delegate to others
|
|
114
|
+
const hasTeams = agent && agent.usesTeams && agent.usesTeams.length > 0;
|
|
115
|
+
response = await this.executeOpenAIStreaming(prompt, fromDelegation, hasTeams, playbook.length, agent, onAction);
|
|
116
|
+
} else {
|
|
117
|
+
response = await this.executeOpenAIWithTools(prompt, tools, agent, fromDelegation, playbook.length);
|
|
118
|
+
}
|
|
119
|
+
} else if (this.provider === 'anthropic') {
|
|
120
|
+
if (useStreaming) {
|
|
121
|
+
response = await this.executeAnthropicStreaming(prompt, agent, onAction);
|
|
122
|
+
} else {
|
|
123
|
+
response = await this.executeAnthropic(prompt, agent);
|
|
124
|
+
}
|
|
125
|
+
} else {
|
|
126
|
+
throw new Error(`Unknown provider: ${this.provider}`);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Check for empty response
|
|
130
|
+
if (!response || response.trim() === '') {
|
|
131
|
+
cliLogger.clear();
|
|
132
|
+
console.error('[LLM] Warning: Received empty response from LLM');
|
|
133
|
+
return { actions: [] };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Clear planning animation
|
|
137
|
+
cliLogger.clear();
|
|
138
|
+
|
|
139
|
+
// Clean markdown code blocks if present
|
|
140
|
+
let cleanedResponse = response.trim();
|
|
141
|
+
if (cleanedResponse.startsWith('```')) {
|
|
142
|
+
// Remove ```json or ``` from start and ``` from end
|
|
143
|
+
cleanedResponse = cleanedResponse.replace(/^```(?:json)?\n?/, '').replace(/\n?```$/, '').trim();
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Try to parse as JSON
|
|
147
|
+
try {
|
|
148
|
+
let parsed = JSON.parse(cleanedResponse);
|
|
149
|
+
|
|
150
|
+
// Unwrap double-escaped JSON in "result" field (common LLM mistake)
|
|
151
|
+
while (parsed.result && typeof parsed.result === 'string') {
|
|
152
|
+
const trimmedResult = parsed.result.trim();
|
|
153
|
+
if (trimmedResult.startsWith('{') || trimmedResult.startsWith('[')) {
|
|
154
|
+
try {
|
|
155
|
+
parsed = JSON.parse(trimmedResult);
|
|
156
|
+
} catch (e) {
|
|
157
|
+
// Can't parse inner - stop unwrapping
|
|
158
|
+
break;
|
|
159
|
+
}
|
|
160
|
+
} else {
|
|
161
|
+
// Not JSON - stop unwrapping
|
|
162
|
+
break;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return parsed;
|
|
167
|
+
} catch (e) {
|
|
168
|
+
console.error('[LLM] Warning: Failed to parse response as JSON');
|
|
169
|
+
console.error(`[LLM] Parse error: ${e.message}`);
|
|
170
|
+
console.error(`[LLM] Response (${cleanedResponse.length} chars): ${cleanedResponse.substring(0, 500)}`);
|
|
171
|
+
if (cleanedResponse.length > 500) {
|
|
172
|
+
console.error(`[LLM] Response end: ...${cleanedResponse.substring(cleanedResponse.length - 200)}`);
|
|
173
|
+
}
|
|
174
|
+
// If parsing fails, return as result
|
|
175
|
+
return { result: cleanedResponse };
|
|
176
|
+
}
|
|
177
|
+
} catch (error) {
|
|
178
|
+
cliLogger.clear();
|
|
179
|
+
cliLogger.error(`[LLM] Error: ${error.message}`);
|
|
180
|
+
if (process.env.KOI_DEBUG_LLM) {
|
|
181
|
+
console.error('[LLM] Full error stack:', error.stack);
|
|
182
|
+
}
|
|
183
|
+
throw error;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
async executeOpenAI(prompt, fromDelegation = false, hasTeams = false, promptLength = 0, agent = null) {
|
|
188
|
+
if (!process.env.OPENAI_API_KEY) {
|
|
189
|
+
throw new Error('OPENAI_API_KEY not set in environment');
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const delegationNote = fromDelegation
|
|
193
|
+
? '\n\nCRITICAL: You are a specialized worker agent. DO NOT return actions, execute your task DIRECTLY.'
|
|
194
|
+
: '';
|
|
195
|
+
|
|
196
|
+
const teamDelegationNote = hasTeams
|
|
197
|
+
? `\n\nIMPORTANT: You have team members available. When tasks involve specialized capabilities (like registry operations, data processing, etc.), use "intent" or "description" fields to delegate to team members instead of using low-level action types. The router will automatically find the right team member based on semantic similarity.
|
|
198
|
+
|
|
199
|
+
CRITICAL: When delegating work that involves MULTIPLE items (e.g., "create these 6 users"):
|
|
200
|
+
- Generate ONE delegation action PER ITEM
|
|
201
|
+
- Each action should contain the data for THAT SPECIFIC ITEM ONLY
|
|
202
|
+
- Example for "create Alice (id=001) and Bob (id=002)":
|
|
203
|
+
{ "title": "Create Alice", "intent": "create user", "data": { "id": "001", "name": "Alice", ... } },
|
|
204
|
+
{ "title": "Create Bob", "intent": "create user", "data": { "id": "002", "name": "Bob", ... } }
|
|
205
|
+
- NEVER group multiple items into one action unless the handler explicitly expects an array`
|
|
206
|
+
: '';
|
|
207
|
+
|
|
208
|
+
const systemPrompt = `You are a Koi agent executor. Your job is to convert user instructions into a precise sequence of executable actions.
|
|
209
|
+
|
|
210
|
+
CRITICAL RULES:
|
|
211
|
+
1. Execute EVERY instruction in the user's request - do not skip any steps
|
|
212
|
+
2. Return ONLY raw JSON - NO markdown, NO wrapping, NO "result" field
|
|
213
|
+
3. Follow the EXACT order of instructions given by the user
|
|
214
|
+
4. NEVER hardcode dynamic data - ALWAYS use template variables:
|
|
215
|
+
- ❌ WRONG: "✅ 6 users created" (hardcoded count)
|
|
216
|
+
- ✅ RIGHT: "✅ \${a1.output.count + a2.output.count + ...} users created" (dynamic)
|
|
217
|
+
- ❌ WRONG: "| Sr | Alice | 30 |" (hardcoded name/age)
|
|
218
|
+
- ✅ RIGHT: "| \${a8.output.users[0].name.endsWith('a') ? 'Sra' : 'Sr'} | \${a8.output.users[0].name} | \${a8.output.users[0].age} |"
|
|
219
|
+
- If you see "X users created" where X is dynamic, replace X with a template expression ONLY for simple arithmetic
|
|
220
|
+
- If you see "{el nombre del usuario}" in instructions, use \${actionId.output.name}, NOT a hardcoded value
|
|
221
|
+
- CRITICAL RULE - COMPLEX CALCULATIONS: If text has placeholders like {x}, {age}, {días}, {dd/mm/yyyy} that need:
|
|
222
|
+
* Age calculations from birthdates
|
|
223
|
+
* Date formatting
|
|
224
|
+
* Time differences
|
|
225
|
+
* Any arithmetic involving dates
|
|
226
|
+
→ MANDATORY: Use format action, NEVER generate template expressions with Date/time calculations
|
|
227
|
+
→ ❌ ABSOLUTELY WRONG: \${new Date(...).getFullYear() - ...} or any Date arithmetic in templates
|
|
228
|
+
→ ✅ ALWAYS RIGHT: { "id": "formatted", "intent": "format", "data": "\${usersArray}", "instruction": "For each user calculate age from birthdate and generate email..." }, then print \${formatted.output.formatted}
|
|
229
|
+
5. NEVER use .map() or arrow functions with nested template literals in template variables:
|
|
230
|
+
- ❌ WRONG: \${array.map(item => \`text \${item.field}\`).join('\\n')} (nested templates cannot be evaluated)
|
|
231
|
+
- ✅ RIGHT: Use format action to transform array data into display text
|
|
232
|
+
- When displaying tables/lists from array data: { "id": "aX", "intent": "format", "data": "\${arrayActionId.output.users}", "instruction": "Format as markdown table with columns: Sr/Sra, Name, Age. Deduce gender from name ending in 'a'" }
|
|
233
|
+
- Then print the formatted result: { "intent": "print", "message": "\${aX.output.formatted}" }
|
|
234
|
+
6. When iterating over arrays, generate actions for ALL elements dynamically
|
|
235
|
+
- NEVER hardcode a fixed number of rows/items when the actual array size might differ
|
|
236
|
+
7. EXTRACT ALL DATA FROM NATURAL LANGUAGE - Parse specifications carefully to get EVERY field:
|
|
237
|
+
- "Alice: id=001, age=30, email=alice@example.com" → { "name": "Alice", "id": "001", "age": 30, "email": "alice@example.com" }
|
|
238
|
+
- "Bob: id=002, de 17 años, bob@example.com" → { "name": "Bob", "id": "002", "age": 17, "email": "bob@example.com" }
|
|
239
|
+
- Pattern: "NAME: property1, property2..." means text before colon is the name
|
|
240
|
+
- Convert natural language ages: "de 17 años" → age: 17, "age is 35" → age: 35
|
|
241
|
+
- NEVER omit fields! If you see a name in the spec, include it in the data object
|
|
242
|
+
8. Use "print" actions to display ALL requested output to the user
|
|
243
|
+
9. ALWAYS use valid JSON - all values must be proper JSON types (strings, numbers, objects, arrays, booleans, null)
|
|
244
|
+
10. EFFICIENCY: Group consecutive print actions into a single print using \\n for line breaks
|
|
245
|
+
- WRONG: Three separate prints for header lines
|
|
246
|
+
- RIGHT: One print with "Line1\\nLine2\\nLine3"
|
|
247
|
+
11. EFFICIENCY - Batch Operations: When performing the same operation on multiple items, check if a batch/plural version exists:
|
|
248
|
+
- Look for plural intent names in available delegation actions: createAllUser/createAllUsers (batch) vs createUser (single)
|
|
249
|
+
- ❌ WRONG: Six separate createUser calls for 6 users
|
|
250
|
+
- ✅ RIGHT: One createAllUser call with array of all 6 users: { "actionType": "delegate", "intent": "createAllUser", "data": { "users": [{name: "Alice", id: "001", ...}, {name: "Bob", id: "002", ...}, ...] } }
|
|
251
|
+
- Apply this principle to ANY repeated operation where a batch version exists
|
|
252
|
+
- Benefits: Fewer network calls, better performance, cleaner action sequences
|
|
253
|
+
12. ACTION IDs - CRITICAL: Add "id" field ONLY to actions that return DATA you need later
|
|
254
|
+
- ✅ PUT IDs ON: delegate actions, registry_get, registry_keys, registry_search (they return data)
|
|
255
|
+
- ❌ NEVER PUT IDs ON: print, log, format, update_state, registry_set, registry_delete (no useful output)
|
|
256
|
+
- Sequential IDs: a1, a2, a3, ... starting fresh for each playbook execution
|
|
257
|
+
- The "id" field goes on THE ACTION THAT PRODUCES THE DATA, not on the action that uses it!
|
|
258
|
+
|
|
259
|
+
EXAMPLES:
|
|
260
|
+
❌ WRONG - ID on print action:
|
|
261
|
+
{ "id": "a1", "actionType": "direct", "intent": "print", "message": "Creating user" },
|
|
262
|
+
{ "actionType": "delegate", "intent": "createUser", "data": {...} },
|
|
263
|
+
{ "actionType": "direct", "intent": "print", "message": "Name: \${a1.output.name}" } ← a1 is print, has no name field!
|
|
264
|
+
|
|
265
|
+
✅ RIGHT - ID on data-producing action:
|
|
266
|
+
{ "actionType": "direct", "intent": "print", "message": "Fetching user..." },
|
|
267
|
+
{ "id": "a1", "actionType": "delegate", "intent": "getUser", "data": {"id": "001"} },
|
|
268
|
+
{ "actionType": "direct", "intent": "print", "message": "Name: \${a1.output.name}" } ← a1 is getUser, has name field!
|
|
269
|
+
|
|
270
|
+
13. RETURN vs FORMAT ACTIONS - CRITICAL: Know when to return raw data vs formatted output:
|
|
271
|
+
- If playbook says "Return: { count, users: [array] }" → MUST return actual JSON array, NOT a formatted string
|
|
272
|
+
- "Transform results to extract user data" from registry_search means reference the .results array directly
|
|
273
|
+
- NEVER use format action before return when the playbook asks for an array
|
|
274
|
+
- Use format action ONLY for final display output to users, NEVER for returning data structures
|
|
275
|
+
|
|
276
|
+
When playbook says "Transform results to extract user data" + "Return: { count, users: [array] }":
|
|
277
|
+
❌ WRONG:
|
|
278
|
+
{ "id": "a1", "intent": "registry_search", "query": {} },
|
|
279
|
+
{ "id": "a2", "intent": "format", "data": "\${a1.output.results}", "instruction": "..." },
|
|
280
|
+
{ "intent": "return", "data": { "users": "\${a2.output.formatted}" } } ← Returns STRING!
|
|
281
|
+
|
|
282
|
+
✅ RIGHT:
|
|
283
|
+
{ "id": "a1", "intent": "registry_search", "query": {} },
|
|
284
|
+
{ "intent": "return", "data": { "count": "\${a1.output.count}", "users": "\${a1.output.results}" } } ← Returns actual array!
|
|
285
|
+
|
|
286
|
+
- registry_search already returns { results: [{key, value}, ...] }, just use that array directly
|
|
287
|
+
- The caller can access individual users with \${actionId.output.users[0].value.name}
|
|
288
|
+
- Only use format when explicitly asked to display/print formatted output
|
|
289
|
+
|
|
290
|
+
${delegationNote}${teamDelegationNote}
|
|
291
|
+
|
|
292
|
+
RESPONSE FORMAT (ALWAYS use this):
|
|
293
|
+
{
|
|
294
|
+
"actions": [
|
|
295
|
+
{ "actionType": "direct", "intent": "print", "message": "Display this to user" },
|
|
296
|
+
{ "actionType": "direct", "intent": "return", "data": {...} }
|
|
297
|
+
]
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
CORRECT EXAMPLES:
|
|
301
|
+
|
|
302
|
+
Example 1 - NEVER hardcode dynamic values (CRITICAL - Follow Rule #4):
|
|
303
|
+
User prompt: "Create 2 users, then show 'X users created' where X is the count"
|
|
304
|
+
❌ WRONG - Hardcoded count:
|
|
305
|
+
{ "actionType": "direct", "intent": "print", "message": "✅ 2 users created" }
|
|
306
|
+
|
|
307
|
+
✅ RIGHT - Dynamic count:
|
|
308
|
+
{ "id": "a1", "actionType": "delegate", "intent": "createUser", "data": {...} },
|
|
309
|
+
{ "id": "a2", "actionType": "delegate", "intent": "createUser", "data": {...} },
|
|
310
|
+
{ "actionType": "direct", "intent": "print", "message": "✅ \${a1.output.success && a2.output.success ? 2 : (a1.output.success || a2.output.success ? 1 : 0)} users created" }
|
|
311
|
+
|
|
312
|
+
Example 2 - Extracting names from natural language (CRITICAL - Follow Rule #6):
|
|
313
|
+
User prompt: "Create Alice: id=001, age=30, email=alice@example.com"
|
|
314
|
+
❌ WRONG - Missing name: { "data": { "id": "001", "age": 30, "email": "alice@example.com" } }
|
|
315
|
+
✅ RIGHT - Include name: { "data": { "name": "Alice", "id": "001", "age": 30, "email": "alice@example.com" } }
|
|
316
|
+
|
|
317
|
+
Example 3 - Delegate with ID:
|
|
318
|
+
{
|
|
319
|
+
"actions": [
|
|
320
|
+
{ "id": "a1", "actionType": "delegate", "intent": "getUser", "data": { "id": "001" } },
|
|
321
|
+
{ "actionType": "direct", "intent": "print", "message": "User: \${a1.output.name}, age \${a1.output.age}" }
|
|
322
|
+
]
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
Example 4 - Multiple actions with IDs:
|
|
326
|
+
{
|
|
327
|
+
"actions": [
|
|
328
|
+
{ "id": "a1", "actionType": "delegate", "intent": "listUsers" },
|
|
329
|
+
{ "actionType": "direct", "intent": "print", "message": "Found \${a1.output.count} users" },
|
|
330
|
+
{ "id": "a2", "actionType": "delegate", "intent": "getUser", "data": { "id": "001" } },
|
|
331
|
+
{ "actionType": "direct", "intent": "print", "message": "First user: \${a2.output.name}" }
|
|
332
|
+
]
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
Example 5 - Registry operations with IDs:
|
|
336
|
+
{
|
|
337
|
+
"actions": [
|
|
338
|
+
{ "id": "a1", "actionType": "direct", "intent": "registry_get", "key": "user:001" },
|
|
339
|
+
{ "actionType": "direct", "intent": "print", "message": "Name: \${a1.output.value.name}" }
|
|
340
|
+
]
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
Example 6 - Without IDs (when results aren't needed):
|
|
344
|
+
{
|
|
345
|
+
"actions": [
|
|
346
|
+
{ "actionType": "direct", "intent": "print", "message": "Hello" },
|
|
347
|
+
{ "actionType": "delegate", "intent": "deleteUser", "data": { "id": "001" } },
|
|
348
|
+
{ "actionType": "direct", "intent": "print", "message": "User deleted" }
|
|
349
|
+
]
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
CRITICAL: ALWAYS include "actionType" field in EVERY action (either "direct" or "delegate")
|
|
353
|
+
|
|
354
|
+
Available actions:
|
|
355
|
+
${actionRegistry.generatePromptDocumentation(agent)}
|
|
356
|
+
${hasTeams && agent ? agent.getPeerCapabilitiesAsActions() : ''}
|
|
357
|
+
|
|
358
|
+
${hasTeams ? `\nIMPORTANT: Do NOT nest "intent" inside "data". The "intent" field must be at the top level.` : ''}
|
|
359
|
+
|
|
360
|
+
Data chaining with action outputs:
|
|
361
|
+
- Use \${a1.output.field} to reference the output of action a1
|
|
362
|
+
- Template variables can ONLY be used INSIDE strings
|
|
363
|
+
- NEVER use template variables as direct values: { "count": \${a1.output.length} } ❌ WRONG
|
|
364
|
+
- ALWAYS quote them: { "count": "\${a1.output.length}" } ✅ CORRECT
|
|
365
|
+
- NEVER use the word "undefined" in JSON - use null or a string instead
|
|
366
|
+
|
|
367
|
+
Examples:
|
|
368
|
+
- \${a1.output.count} - Access count field from action a1
|
|
369
|
+
- \${a2.output.users} - Access users array from action a2
|
|
370
|
+
- \${a3.output.users[0].name} - Access nested field
|
|
371
|
+
- After action a5 executes, you can reference \${a5.output} in subsequent actions
|
|
372
|
+
|
|
373
|
+
CRITICAL: When instructions say "Do NOT add print actions", follow that EXACTLY - only generate the actions listed in the steps.
|
|
374
|
+
When using "return" actions with data containing template variables, do NOT add intermediate print actions - they will break the data chain.
|
|
375
|
+
|
|
376
|
+
REMEMBER: Include print actions for ALL output the user should see, UNLESS the instructions explicitly say not to. Return valid, parseable JSON only.`;
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
// Use fastest model for delegated work or short playbooks
|
|
380
|
+
const model = fromDelegation || promptLength < 500 ? 'gpt-4o-mini' : this.model;
|
|
381
|
+
|
|
382
|
+
if (process.env.KOI_DEBUG_LLM) {
|
|
383
|
+
const agentInfo = agent ? ` | Agent: ${agent.name}` : '';
|
|
384
|
+
console.error('─'.repeat(80));
|
|
385
|
+
console.error(`[LLM Debug] executeOpenAI - Model: ${model}${agentInfo}`);
|
|
386
|
+
console.error('System Prompt:');
|
|
387
|
+
console.error(formatPromptForDebug(systemPrompt));
|
|
388
|
+
console.error('============');
|
|
389
|
+
console.error('User Prompt:');
|
|
390
|
+
console.error('============');
|
|
391
|
+
console.error(formatPromptForDebug(prompt));
|
|
392
|
+
console.error('─'.repeat(80));
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
const completion = await this.openai.chat.completions.create({
|
|
396
|
+
model,
|
|
397
|
+
messages: [
|
|
398
|
+
{
|
|
399
|
+
role: 'system',
|
|
400
|
+
content: systemPrompt
|
|
401
|
+
},
|
|
402
|
+
{
|
|
403
|
+
role: 'user',
|
|
404
|
+
content: prompt
|
|
405
|
+
}
|
|
406
|
+
],
|
|
407
|
+
temperature: 0, // Always use 0 for maximum determinism
|
|
408
|
+
max_tokens: this.maxTokens,
|
|
409
|
+
response_format: { type: "json_object" } // Force valid JSON responses
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
const content = completion.choices[0].message.content?.trim() || '';
|
|
413
|
+
|
|
414
|
+
if (process.env.KOI_DEBUG_LLM) {
|
|
415
|
+
console.error(`[LLM Debug] executeOpenAI Response (${content.length} chars):`);
|
|
416
|
+
console.error('\x1b[90m' + content + '\x1b[0m');
|
|
417
|
+
console.error('─'.repeat(80));
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
if (!content) {
|
|
421
|
+
console.error('[LLM] Warning: executeOpenAI returned empty content');
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
return content;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
async executeOpenAIWithTools(prompt, tools = [], agent = null, fromDelegation = false, promptLength = 0) {
|
|
428
|
+
if (!process.env.OPENAI_API_KEY) {
|
|
429
|
+
throw new Error('OPENAI_API_KEY not set in environment');
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// If no tools available, fallback to regular execution
|
|
433
|
+
if (!tools || tools.length === 0) {
|
|
434
|
+
// hasTeams should only be true if agent can delegate to others (uses teams as a client)
|
|
435
|
+
// NOT if agent is just a member of a team (has peers)
|
|
436
|
+
const hasTeams = agent && agent.usesTeams && agent.usesTeams.length > 0;
|
|
437
|
+
return await this.executeOpenAI(prompt, fromDelegation, hasTeams, promptLength, agent);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// Convert tools to OpenAI format
|
|
441
|
+
const openAITools = tools.map(tool => {
|
|
442
|
+
// Build a more informative description
|
|
443
|
+
const description = tool.description || `Function ${tool.name}`;
|
|
444
|
+
const enhancedDescription = `${description}. Extract necessary parameters from the prompt and context.`;
|
|
445
|
+
|
|
446
|
+
// Define common parameter properties that skills might need
|
|
447
|
+
// This helps OpenAI understand what to extract from the context
|
|
448
|
+
const commonProperties = {
|
|
449
|
+
url: {
|
|
450
|
+
type: 'string',
|
|
451
|
+
description: 'URL to fetch or process (extract from prompt or context)'
|
|
452
|
+
},
|
|
453
|
+
text: {
|
|
454
|
+
type: 'string',
|
|
455
|
+
description: 'Text content to process (extract from prompt or context)'
|
|
456
|
+
},
|
|
457
|
+
content: {
|
|
458
|
+
type: 'string',
|
|
459
|
+
description: 'Content to process (extract from prompt or context)'
|
|
460
|
+
},
|
|
461
|
+
numbers: {
|
|
462
|
+
type: 'array',
|
|
463
|
+
items: { type: 'number' },
|
|
464
|
+
description: 'Array of numbers to process (extract from prompt or context)'
|
|
465
|
+
},
|
|
466
|
+
data: {
|
|
467
|
+
type: 'object',
|
|
468
|
+
description: 'Additional data (extract from context)'
|
|
469
|
+
}
|
|
470
|
+
};
|
|
471
|
+
|
|
472
|
+
return {
|
|
473
|
+
type: 'function',
|
|
474
|
+
function: {
|
|
475
|
+
name: tool.name,
|
|
476
|
+
description: enhancedDescription,
|
|
477
|
+
parameters: {
|
|
478
|
+
type: 'object',
|
|
479
|
+
properties: commonProperties,
|
|
480
|
+
additionalProperties: true,
|
|
481
|
+
required: []
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
};
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
const delegationInstructions = fromDelegation
|
|
488
|
+
? `\n\nCRITICAL: You are a specialized worker agent being called from another agent.
|
|
489
|
+
DO NOT return actions for delegation. Execute your task DIRECTLY using available tools or your own capabilities.
|
|
490
|
+
Return only the direct result of your work.`
|
|
491
|
+
: `\n\n2. DELEGATION (for multi-step or complex tasks):
|
|
492
|
+
- When a task requires multiple steps or different capabilities, delegate by returning actions
|
|
493
|
+
- Each action describes what needs to be done, and the framework will find the right agent
|
|
494
|
+
- You can include additional fields in your response (like "plan", "explanation", etc.) along with the actions
|
|
495
|
+
- Format:
|
|
496
|
+
{
|
|
497
|
+
"plan": "Optional: description of the approach you're taking",
|
|
498
|
+
"actions": [
|
|
499
|
+
{ "title": "Fetching web page...", "intent": "fetch web page", "data": { "url": "..." } },
|
|
500
|
+
{ "title": "Summarizing...", "intent": "summarize content", "data": { "content": "\${previousResult.content}" } }
|
|
501
|
+
]
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
CRITICAL: When delegating work that involves MULTIPLE items (e.g., "create these 6 users"):
|
|
505
|
+
- Generate ONE delegation action PER ITEM
|
|
506
|
+
- Each action should contain the data for THAT SPECIFIC ITEM ONLY
|
|
507
|
+
- Example for "create Alice (id=001) and Bob (id=002)":
|
|
508
|
+
{ "title": "Create Alice", "intent": "create user", "data": { "id": "001", "name": "Alice", ... } },
|
|
509
|
+
{ "title": "Create Bob", "intent": "create user", "data": { "id": "002", "name": "Bob", ... } }
|
|
510
|
+
- NEVER group multiple items into one action unless the handler explicitly expects an array`;
|
|
511
|
+
|
|
512
|
+
const systemPrompt = `You are a helpful assistant in the Koi agent orchestration framework.
|
|
513
|
+
|
|
514
|
+
You can accomplish tasks in ${fromDelegation ? 'ONE' : 'TWO'} way${fromDelegation ? '' : 's'}:
|
|
515
|
+
|
|
516
|
+
1. DIRECT EXECUTION (for single-step tasks):
|
|
517
|
+
- Use available tools/functions when you have a tool that does exactly what's needed
|
|
518
|
+
- Extract parameters from the prompt or context and pass them to the tool
|
|
519
|
+
- Example: If you have a "fetchUrl" tool and need to download a webpage, call fetchUrl({ url: "..." })
|
|
520
|
+
${delegationInstructions}
|
|
521
|
+
|
|
522
|
+
CRITICAL INSTRUCTIONS FOR TOOL CALLING:
|
|
523
|
+
1. When you see a URL, email, text, or any data in the prompt or context that is needed for a tool, YOU MUST pass it as parameters to the function call.
|
|
524
|
+
2. Extract parameters from:
|
|
525
|
+
- The explicit instructions in the prompt (e.g., "Download the web page from this URL: https://example.com")
|
|
526
|
+
- The Context section showing available data
|
|
527
|
+
3. DO NOT call tools with empty parameters. Always extract and pass the necessary data.
|
|
528
|
+
4. Match parameter names to what makes sense (url, text, email, etc.)
|
|
529
|
+
5. After calling a tool, return the tool result DIRECTLY as JSON. DO NOT wrap it in a "result" field or stringify it again.
|
|
530
|
+
|
|
531
|
+
JSON VALIDATION RULES:
|
|
532
|
+
- ALWAYS use valid JSON - all values must be proper JSON types (strings, numbers, objects, arrays, booleans, null)
|
|
533
|
+
- Template variables like \${previousResult.field} can ONLY be used INSIDE strings
|
|
534
|
+
- NEVER use template variables as direct values: { "count": \${previousResult.length} } ❌ WRONG
|
|
535
|
+
- ALWAYS quote them: { "count": "\${previousResult.length}" } ✅ CORRECT
|
|
536
|
+
- NEVER use the word "undefined" in JSON - use null or a string instead
|
|
537
|
+
|
|
538
|
+
Examples:
|
|
539
|
+
- Prompt: "Download the web page from this URL: https://example.com"
|
|
540
|
+
- Context: { "args": { "url": "https://example.com" } }
|
|
541
|
+
- Correct tool call: fetchUrl({ "url": "https://example.com" })
|
|
542
|
+
- WRONG: fetchUrl({})
|
|
543
|
+
- After tool returns { "url": "...", "content": "..." }, return it directly as-is
|
|
544
|
+
|
|
545
|
+
You respond with valid JSON only. No markdown, no code blocks, no explanations.`;
|
|
546
|
+
|
|
547
|
+
const messages = [
|
|
548
|
+
{ role: 'system', content: systemPrompt },
|
|
549
|
+
{ role: 'user', content: prompt }
|
|
550
|
+
];
|
|
551
|
+
|
|
552
|
+
// Use fastest model for delegated work or short prompts
|
|
553
|
+
const model = fromDelegation || promptLength < 500 ? 'gpt-4o-mini' : this.model;
|
|
554
|
+
|
|
555
|
+
if (process.env.KOI_DEBUG_LLM) {
|
|
556
|
+
const agentInfo = agent ? ` | Agent: ${agent.name}` : '';
|
|
557
|
+
console.error('─'.repeat(80));
|
|
558
|
+
console.error(`[LLM Debug] executeOpenAIWithTools - Model: ${model}, Tools: ${openAITools.length}${agentInfo}`);
|
|
559
|
+
console.error('System Prompt:');
|
|
560
|
+
console.error(formatPromptForDebug(systemPrompt));
|
|
561
|
+
console.error('============');
|
|
562
|
+
console.error('User Prompt:');
|
|
563
|
+
console.error('============');
|
|
564
|
+
console.error(formatPromptForDebug(prompt));
|
|
565
|
+
console.error('─'.repeat(80));
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
// Call OpenAI with tools
|
|
569
|
+
let completion = await this.openai.chat.completions.create({
|
|
570
|
+
model,
|
|
571
|
+
messages,
|
|
572
|
+
tools: openAITools,
|
|
573
|
+
tool_choice: 'auto',
|
|
574
|
+
temperature: 0, // Always use 0 for maximum determinism
|
|
575
|
+
max_tokens: this.maxTokens,
|
|
576
|
+
response_format: { type: "json_object" } // Force valid JSON responses
|
|
577
|
+
});
|
|
578
|
+
|
|
579
|
+
let message = completion.choices[0].message;
|
|
580
|
+
|
|
581
|
+
// Handle tool calls
|
|
582
|
+
if (message.tool_calls && message.tool_calls.length > 0) {
|
|
583
|
+
// Execute tool calls
|
|
584
|
+
messages.push(message); // Add assistant's response with tool calls
|
|
585
|
+
|
|
586
|
+
let toolResults = [];
|
|
587
|
+
for (const toolCall of message.tool_calls) {
|
|
588
|
+
const tool = tools.find(t => t.name === toolCall.function.name);
|
|
589
|
+
if (tool) {
|
|
590
|
+
try {
|
|
591
|
+
// Show that agent is using a skill
|
|
592
|
+
if (agent) {
|
|
593
|
+
cliLogger.progress(`[🤖 ${agent.name} ⚙️ ${tool.name}]`);
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
// Parse arguments - OpenAI sends them as JSON string
|
|
597
|
+
const args = JSON.parse(toolCall.function.arguments);
|
|
598
|
+
|
|
599
|
+
// Execute the function with the arguments
|
|
600
|
+
const result = await tool.fn(args);
|
|
601
|
+
toolResults.push(result);
|
|
602
|
+
|
|
603
|
+
// Clear progress after tool execution
|
|
604
|
+
cliLogger.clear();
|
|
605
|
+
|
|
606
|
+
// Add tool result to messages
|
|
607
|
+
messages.push({
|
|
608
|
+
role: 'tool',
|
|
609
|
+
tool_call_id: toolCall.id,
|
|
610
|
+
content: JSON.stringify(result)
|
|
611
|
+
});
|
|
612
|
+
} catch (error) {
|
|
613
|
+
cliLogger.clear();
|
|
614
|
+
const errorResult = { error: error.message };
|
|
615
|
+
toolResults.push(errorResult);
|
|
616
|
+
messages.push({
|
|
617
|
+
role: 'tool',
|
|
618
|
+
tool_call_id: toolCall.id,
|
|
619
|
+
content: JSON.stringify(errorResult)
|
|
620
|
+
});
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
// If this is a delegated call with a single tool call, return the tool result directly
|
|
626
|
+
// This avoids token limit issues when the result is large (e.g., fetched HTML content)
|
|
627
|
+
if (fromDelegation && toolResults.length === 1 && !toolResults[0].error) {
|
|
628
|
+
return JSON.stringify(toolResults[0]);
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
// Call OpenAI again with tool results
|
|
632
|
+
completion = await this.openai.chat.completions.create({
|
|
633
|
+
model, // Use same model as initial call
|
|
634
|
+
messages,
|
|
635
|
+
temperature: 0, // Always use 0 for maximum determinism
|
|
636
|
+
max_tokens: this.maxTokens,
|
|
637
|
+
response_format: { type: "json_object" } // Force valid JSON responses
|
|
638
|
+
});
|
|
639
|
+
|
|
640
|
+
message = completion.choices[0].message;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
const finalContent = message.content?.trim() || '';
|
|
644
|
+
|
|
645
|
+
if (process.env.KOI_DEBUG_LLM) {
|
|
646
|
+
console.error(`[LLM Debug] executeOpenAIWithTools Response (${finalContent.length} chars):`);
|
|
647
|
+
console.error('\x1b[90m' + finalContent + '\x1b[0m');
|
|
648
|
+
console.error('─'.repeat(80));
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
if (!finalContent) {
|
|
652
|
+
console.error('[LLM] Warning: OpenAI returned empty content');
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
return finalContent;
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
async executeAnthropic(prompt, agent = null) {
|
|
659
|
+
if (!process.env.ANTHROPIC_API_KEY) {
|
|
660
|
+
throw new Error('ANTHROPIC_API_KEY not set in environment');
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
// Check if agent has teams for delegation
|
|
664
|
+
const hasTeams = agent && agent.usesTeams && agent.usesTeams.length > 0;
|
|
665
|
+
|
|
666
|
+
const systemPrompt = `You are a Koi agent executor. Your job is to convert user instructions into a precise sequence of executable actions.
|
|
667
|
+
|
|
668
|
+
CRITICAL RULES:
|
|
669
|
+
1. Execute EVERY instruction in the user's request - do not skip any steps
|
|
670
|
+
2. Return ONLY raw JSON - NO markdown, NO wrapping, NO "result" field
|
|
671
|
+
3. Follow the EXACT order of instructions given by the user
|
|
672
|
+
4. Use "print" actions to display ALL requested output to the user
|
|
673
|
+
5. ALWAYS use valid JSON - all values must be proper JSON types (strings, numbers, objects, arrays, booleans, null)
|
|
674
|
+
6. EFFICIENCY: Group consecutive print actions into a single print using \\n for line breaks
|
|
675
|
+
- WRONG: Three separate prints for header lines
|
|
676
|
+
- RIGHT: One print with "Line1\\nLine2\\nLine3"
|
|
677
|
+
6b. EFFICIENCY - Batch Operations: When performing the same operation on multiple items, check if a batch/plural version exists:
|
|
678
|
+
- Look for plural intent names in available delegation actions: createAllUser/createAllUsers (batch) vs createUser (single)
|
|
679
|
+
- ❌ WRONG: Six separate createUser calls for 6 users
|
|
680
|
+
- ✅ RIGHT: One createAllUser call with array of all 6 users: { "actionType": "delegate", "intent": "createAllUser", "data": { "users": [{name: "Alice", id: "001", ...}, {name: "Bob", id: "002", ...}, ...] } }
|
|
681
|
+
- Apply this principle to ANY repeated operation where a batch version exists
|
|
682
|
+
- Benefits: Fewer network calls, better performance, cleaner action sequences
|
|
683
|
+
7. ACTION IDs (OPTIONAL): Use "id" field ONLY when you need to reference the result later
|
|
684
|
+
- Add "id": "a1" only if you'll use \${a1.output} in a future action
|
|
685
|
+
- Actions without "id" won't save their output (use for print, one-time actions)
|
|
686
|
+
- Sequential IDs: a1, a2, a3, ... (only for actions that need saving)
|
|
687
|
+
- Example: { "id": "a1", "intent": "getUser" } → later use \${a1.output.name}
|
|
688
|
+
8. CRITICAL - DATE/AGE CALCULATIONS & TEXT GENERATION: If playbook contains text templates with {x}, {age}, {días}, {dd/mm/yyyy} or any placeholder:
|
|
689
|
+
- NEVER generate template expressions with Date arithmetic
|
|
690
|
+
- MANDATORY: Use format action with the data array
|
|
691
|
+
- CRITICAL: Copy the COMPLETE template from playbook to format action's "instruction" - preserve ALL details:
|
|
692
|
+
* Keep ALL conditional logic (e.g., "Estimado o Estimada si es chica, deducelo por el nombre")
|
|
693
|
+
* Preserve ALL line breaks/spacing (use \n in instruction string for each line break in template)
|
|
694
|
+
* Keep original language and exact wording
|
|
695
|
+
* Don't simplify, paraphrase, or omit any part of the template
|
|
696
|
+
- ❌ ABSOLUTELY WRONG: print with "\${2023 - new Date(birthdate).getFullYear()}" or any Date calculations
|
|
697
|
+
- ❌ WRONG: Simplifying template from "Estimado (o Estimada si es chica) <nombre>,\n\nComo sabemos..." to "Estimado {name}, Como sabemos..."
|
|
698
|
+
- ✅ RIGHT: { "id": "formatted", "intent": "format", "data": "\${usersArray}", "instruction": "For each user write:\n\nEstimado (o Estimada si es chica, deducelo por el nombre) {name},\n\nComo sabemos que usted tiene {age} años, le queremos dar la enhorabuena!\n\nAtentamente,\nLa empresa!!\n\nSeparate emails with blank line" }, then print "\${formatted.output.formatted}"
|
|
699
|
+
9. NEVER use .map() or arrow functions with nested template literals in template variables:
|
|
700
|
+
- ❌ ABSOLUTELY WRONG: \${array.map(item => \`text \${item.field}\`).join('\\n')} (nested templates CANNOT be evaluated)
|
|
701
|
+
- ❌ WRONG: print with "\${users.map(u => \`| \${u.name} | \${u.age} |\`).join('\\n')}" (will print literal template string)
|
|
702
|
+
- ✅ MANDATORY: Use format action for ANY iteration over arrays
|
|
703
|
+
- For markdown tables: { "id": "aX", "intent": "format", "data": "\${arrayId.output.users}", "instruction": "Generate markdown table with columns: Sr/Sra (deduce from name), Name, Age. Include header row with | Sr/Sra | Name | Age | and separator |--------|------|-----|" }
|
|
704
|
+
- For lists: { "id": "aX", "intent": "format", "data": "\${arrayId.output.items}", "instruction": "Format each item as: - {name}: {description}" }
|
|
705
|
+
- Then print: { "intent": "print", "message": "\${aX.output.formatted}" }
|
|
706
|
+
|
|
707
|
+
RESPONSE FORMAT (ALWAYS use this):
|
|
708
|
+
{
|
|
709
|
+
"actions": [
|
|
710
|
+
{ "actionType": "direct", "intent": "print", "message": "Display this to user" },
|
|
711
|
+
{ "actionType": "direct", "intent": "return", "data": {...} }
|
|
712
|
+
]
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
CORRECT EXAMPLES:
|
|
716
|
+
|
|
717
|
+
Example 1 - NEVER hardcode dynamic values (CRITICAL - Follow Rule #4):
|
|
718
|
+
User prompt: "Create 2 users, then show 'X users created' where X is the count"
|
|
719
|
+
❌ WRONG - Hardcoded count:
|
|
720
|
+
{ "actionType": "direct", "intent": "print", "message": "✅ 2 users created" }
|
|
721
|
+
|
|
722
|
+
✅ RIGHT - Dynamic count:
|
|
723
|
+
{ "id": "a1", "actionType": "delegate", "intent": "createUser", "data": {...} },
|
|
724
|
+
{ "id": "a2", "actionType": "delegate", "intent": "createUser", "data": {...} },
|
|
725
|
+
{ "actionType": "direct", "intent": "print", "message": "✅ \${a1.output.success && a2.output.success ? 2 : (a1.output.success || a2.output.success ? 1 : 0)} users created" }
|
|
726
|
+
|
|
727
|
+
Example 2 - Extracting names from natural language (CRITICAL - Follow Rule #6):
|
|
728
|
+
User prompt: "Create Alice: id=001, age=30, email=alice@example.com"
|
|
729
|
+
❌ WRONG - Missing name: { "data": { "id": "001", "age": 30, "email": "alice@example.com" } }
|
|
730
|
+
✅ RIGHT - Include name: { "data": { "name": "Alice", "id": "001", "age": 30, "email": "alice@example.com" } }
|
|
731
|
+
|
|
732
|
+
Example 3 - Delegate with ID:
|
|
733
|
+
{
|
|
734
|
+
"actions": [
|
|
735
|
+
{ "id": "a1", "actionType": "delegate", "intent": "getUser", "data": { "id": "001" } },
|
|
736
|
+
{ "actionType": "direct", "intent": "print", "message": "User: \${a1.output.name}, age \${a1.output.age}" }
|
|
737
|
+
]
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
Example 4 - Multiple actions with IDs:
|
|
741
|
+
{
|
|
742
|
+
"actions": [
|
|
743
|
+
{ "id": "a1", "actionType": "delegate", "intent": "listUsers" },
|
|
744
|
+
{ "actionType": "direct", "intent": "print", "message": "Found \${a1.output.count} users" },
|
|
745
|
+
{ "id": "a2", "actionType": "delegate", "intent": "getUser", "data": { "id": "001" } },
|
|
746
|
+
{ "actionType": "direct", "intent": "print", "message": "First user: \${a2.output.name}" }
|
|
747
|
+
]
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
Example 5 - Registry operations with IDs:
|
|
751
|
+
{
|
|
752
|
+
"actions": [
|
|
753
|
+
{ "id": "a1", "actionType": "direct", "intent": "registry_get", "key": "user:001" },
|
|
754
|
+
{ "actionType": "direct", "intent": "print", "message": "Name: \${a1.output.value.name}" }
|
|
755
|
+
]
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
Example 6 - Without IDs (when results aren't needed):
|
|
759
|
+
{
|
|
760
|
+
"actions": [
|
|
761
|
+
{ "actionType": "direct", "intent": "print", "message": "Hello" },
|
|
762
|
+
{ "actionType": "delegate", "intent": "deleteUser", "data": { "id": "001" } },
|
|
763
|
+
{ "actionType": "direct", "intent": "print", "message": "User deleted" }
|
|
764
|
+
]
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
CRITICAL: ALWAYS include "actionType" field in EVERY action (either "direct" or "delegate")
|
|
768
|
+
|
|
769
|
+
Available actions:
|
|
770
|
+
${actionRegistry.generatePromptDocumentation(agent)}
|
|
771
|
+
${hasTeams && agent ? agent.getPeerCapabilitiesAsActions() : ''}
|
|
772
|
+
|
|
773
|
+
${hasTeams ? `\nIMPORTANT: Do NOT nest "intent" inside "data". The "intent" field must be at the top level.` : ''}
|
|
774
|
+
|
|
775
|
+
Data chaining with action outputs:
|
|
776
|
+
- Use \${a1.output.field} to reference the output of action a1
|
|
777
|
+
- Template variables can ONLY be used INSIDE strings
|
|
778
|
+
- NEVER use template variables as direct values: { "count": \${a1.output.length} } ❌ WRONG
|
|
779
|
+
- ALWAYS quote them: { "count": "\${a1.output.length}" } ✅ CORRECT
|
|
780
|
+
- NEVER use the word "undefined" in JSON - use null or a string instead
|
|
781
|
+
|
|
782
|
+
Examples:
|
|
783
|
+
- \${a1.output.count} - Access count field from action a1
|
|
784
|
+
- \${a2.output.users} - Access users array from action a2
|
|
785
|
+
- \${a3.output.users[0].name} - Access nested field
|
|
786
|
+
- After action a5 executes, you can reference \${a5.output} in subsequent actions
|
|
787
|
+
|
|
788
|
+
CRITICAL: When instructions say "Do NOT add print actions", follow that EXACTLY - only generate the actions listed in the steps.
|
|
789
|
+
When using "return" actions with data containing template variables, do NOT add intermediate print actions - they will break the data chain.
|
|
790
|
+
|
|
791
|
+
REMEMBER: Include print actions for ALL output the user should see, UNLESS the instructions explicitly say not to. Return valid, parseable JSON only.`;
|
|
792
|
+
|
|
793
|
+
const message = await this.anthropic.messages.create({
|
|
794
|
+
model: this.model,
|
|
795
|
+
max_tokens: this.maxTokens,
|
|
796
|
+
temperature: 0, // Always use 0 for maximum determinism
|
|
797
|
+
system: systemPrompt,
|
|
798
|
+
messages: [
|
|
799
|
+
{
|
|
800
|
+
role: 'user',
|
|
801
|
+
content: prompt
|
|
802
|
+
}
|
|
803
|
+
]
|
|
804
|
+
});
|
|
805
|
+
|
|
806
|
+
return message.content[0].text.trim();
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
/**
|
|
810
|
+
* Execute OpenAI call with streaming and incremental action execution
|
|
811
|
+
* @param {string} prompt - The prompt to send
|
|
812
|
+
* @param {boolean} fromDelegation - Whether this is from delegation
|
|
813
|
+
* @param {boolean} hasTeams - Whether agent has teams
|
|
814
|
+
* @param {number} promptLength - Length of prompt for model selection
|
|
815
|
+
* @param {Object} agent - Agent instance
|
|
816
|
+
* @param {Function} onAction - Callback called for each complete action: (action) => void
|
|
817
|
+
* @returns {Promise<Object>} - Final parsed response
|
|
818
|
+
*/
|
|
819
|
+
async executeOpenAIStreaming(prompt, fromDelegation = false, hasTeams = false, promptLength = 0, agent = null, onAction = null) {
|
|
820
|
+
if (!process.env.OPENAI_API_KEY) {
|
|
821
|
+
throw new Error('OPENAI_API_KEY not set in environment');
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
// Build system prompt
|
|
825
|
+
const systemPrompt = `You are a Koi agent executor. Your job is to convert user instructions into a precise sequence of executable actions.
|
|
826
|
+
|
|
827
|
+
CRITICAL RULES:
|
|
828
|
+
1. Execute EVERY instruction in the user's request - do not skip any steps
|
|
829
|
+
2. Return ONLY raw JSON - NO markdown, NO wrapping, NO "result" field
|
|
830
|
+
3. Follow the EXACT order of instructions given by the user
|
|
831
|
+
4. Use "print" actions to display ALL requested output to the user
|
|
832
|
+
5. ALWAYS use valid JSON - all values must be proper JSON types (strings, numbers, objects, arrays, booleans, null)
|
|
833
|
+
6. EFFICIENCY: Group consecutive print actions into a single print using \\n for line breaks
|
|
834
|
+
- WRONG: Three separate prints for header lines
|
|
835
|
+
- RIGHT: One print with "Line1\\nLine2\\nLine3"
|
|
836
|
+
6b. EFFICIENCY - Batch Operations: When performing the same operation on multiple items, check if a batch/plural version exists:
|
|
837
|
+
- Look for plural intent names in available delegation actions: createAllUser/createAllUsers (batch) vs createUser (single)
|
|
838
|
+
- ❌ WRONG: Six separate createUser calls for 6 users
|
|
839
|
+
- ✅ RIGHT: One createAllUser call with array of all 6 users: { "actionType": "delegate", "intent": "createAllUser", "data": { "users": [{name: "Alice", id: "001", ...}, {name: "Bob", id: "002", ...}, ...] } }
|
|
840
|
+
- Apply this principle to ANY repeated operation where a batch version exists
|
|
841
|
+
- Benefits: Fewer network calls, better performance, cleaner action sequences
|
|
842
|
+
7. ACTION IDs (OPTIONAL): Use "id" field ONLY when you need to reference the result later
|
|
843
|
+
- Add "id": "a1" only if you'll use \${a1.output} in a future action
|
|
844
|
+
- Actions without "id" won't save their output (use for print, one-time actions)
|
|
845
|
+
- Sequential IDs: a1, a2, a3, ... (only for actions that need saving)
|
|
846
|
+
- Example: { "id": "a1", "intent": "getUser" } → later use \${a1.output.name}
|
|
847
|
+
8. CRITICAL - DATE/AGE CALCULATIONS & TEXT GENERATION: If playbook contains text templates with {x}, {age}, {días}, {dd/mm/yyyy} or any placeholder:
|
|
848
|
+
- NEVER generate template expressions with Date arithmetic
|
|
849
|
+
- MANDATORY: Use format action with the data array
|
|
850
|
+
- CRITICAL: Copy the COMPLETE template from playbook to format action's "instruction" - preserve ALL details:
|
|
851
|
+
* Keep ALL conditional logic (e.g., "Estimado o Estimada si es chica, deducelo por el nombre")
|
|
852
|
+
* Preserve ALL line breaks/spacing (use \n in instruction string for each line break in template)
|
|
853
|
+
* Keep original language and exact wording
|
|
854
|
+
* Don't simplify, paraphrase, or omit any part of the template
|
|
855
|
+
- ❌ ABSOLUTELY WRONG: print with "\${2023 - new Date(birthdate).getFullYear()}" or any Date calculations
|
|
856
|
+
- ❌ WRONG: Simplifying template from "Estimado (o Estimada si es chica) <nombre>,\n\nComo sabemos..." to "Estimado {name}, Como sabemos..."
|
|
857
|
+
- ✅ RIGHT: { "id": "formatted", "intent": "format", "data": "\${usersArray}", "instruction": "For each user write:\n\nEstimado (o Estimada si es chica, deducelo por el nombre) {name},\n\nComo sabemos que usted tiene {age} años, le queremos dar la enhorabuena!\n\nAtentamente,\nLa empresa!!\n\nSeparate emails with blank line" }, then print "\${formatted.output.formatted}"
|
|
858
|
+
9. NEVER use .map() or arrow functions with nested template literals in template variables:
|
|
859
|
+
- ❌ ABSOLUTELY WRONG: \${array.map(item => \`text \${item.field}\`).join('\\n')} (nested templates CANNOT be evaluated)
|
|
860
|
+
- ❌ WRONG: print with "\${users.map(u => \`| \${u.name} | \${u.age} |\`).join('\\n')}" (will print literal template string)
|
|
861
|
+
- ✅ MANDATORY: Use format action for ANY iteration over arrays
|
|
862
|
+
- For markdown tables: { "id": "aX", "intent": "format", "data": "\${arrayId.output.users}", "instruction": "Generate markdown table with columns: Sr/Sra (deduce from name), Name, Age. Include header row with | Sr/Sra | Name | Age | and separator |--------|------|-----|" }
|
|
863
|
+
- For lists: { "id": "aX", "intent": "format", "data": "\${arrayId.output.items}", "instruction": "Format each item as: - {name}: {description}" }
|
|
864
|
+
- Then print: { "intent": "print", "message": "\${aX.output.formatted}" }
|
|
865
|
+
|
|
866
|
+
RESPONSE FORMAT (ALWAYS use this):
|
|
867
|
+
{
|
|
868
|
+
"actions": [
|
|
869
|
+
{ "actionType": "direct", "intent": "print", "message": "Display this to user" },
|
|
870
|
+
{ "actionType": "direct", "intent": "return", "data": {...} }
|
|
871
|
+
]
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
CORRECT EXAMPLES:
|
|
875
|
+
|
|
876
|
+
Example 1 - NEVER hardcode dynamic values (CRITICAL - Follow Rule #4):
|
|
877
|
+
User prompt: "Create 2 users, then show 'X users created' where X is the count"
|
|
878
|
+
❌ WRONG - Hardcoded count:
|
|
879
|
+
{ "actionType": "direct", "intent": "print", "message": "✅ 2 users created" }
|
|
880
|
+
|
|
881
|
+
✅ RIGHT - Dynamic count:
|
|
882
|
+
{ "id": "a1", "actionType": "delegate", "intent": "createUser", "data": {...} },
|
|
883
|
+
{ "id": "a2", "actionType": "delegate", "intent": "createUser", "data": {...} },
|
|
884
|
+
{ "actionType": "direct", "intent": "print", "message": "✅ \${a1.output.success && a2.output.success ? 2 : (a1.output.success || a2.output.success ? 1 : 0)} users created" }
|
|
885
|
+
|
|
886
|
+
Example 2 - Extracting names from natural language (CRITICAL - Follow Rule #6):
|
|
887
|
+
User prompt: "Create Alice: id=001, age=30, email=alice@example.com"
|
|
888
|
+
❌ WRONG - Missing name: { "data": { "id": "001", "age": 30, "email": "alice@example.com" } }
|
|
889
|
+
✅ RIGHT - Include name: { "data": { "name": "Alice", "id": "001", "age": 30, "email": "alice@example.com" } }
|
|
890
|
+
|
|
891
|
+
Example 3 - Delegate with ID:
|
|
892
|
+
{
|
|
893
|
+
"actions": [
|
|
894
|
+
{ "id": "a1", "actionType": "delegate", "intent": "getUser", "data": { "id": "001" } },
|
|
895
|
+
{ "actionType": "direct", "intent": "print", "message": "User: \${a1.output.name}, age \${a1.output.age}" }
|
|
896
|
+
]
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
Example 4 - Multiple actions with IDs:
|
|
900
|
+
{
|
|
901
|
+
"actions": [
|
|
902
|
+
{ "id": "a1", "actionType": "delegate", "intent": "listUsers" },
|
|
903
|
+
{ "actionType": "direct", "intent": "print", "message": "Found \${a1.output.count} users" },
|
|
904
|
+
{ "id": "a2", "actionType": "delegate", "intent": "getUser", "data": { "id": "001" } },
|
|
905
|
+
{ "actionType": "direct", "intent": "print", "message": "First user: \${a2.output.name}" }
|
|
906
|
+
]
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
Example 5 - Registry operations with IDs:
|
|
910
|
+
{
|
|
911
|
+
"actions": [
|
|
912
|
+
{ "id": "a1", "actionType": "direct", "intent": "registry_get", "key": "user:001" },
|
|
913
|
+
{ "actionType": "direct", "intent": "print", "message": "Name: \${a1.output.value.name}" }
|
|
914
|
+
]
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
Example 6 - Without IDs (when results aren't needed):
|
|
918
|
+
{
|
|
919
|
+
"actions": [
|
|
920
|
+
{ "actionType": "direct", "intent": "print", "message": "Hello" },
|
|
921
|
+
{ "actionType": "delegate", "intent": "deleteUser", "data": { "id": "001" } },
|
|
922
|
+
{ "actionType": "direct", "intent": "print", "message": "User deleted" }
|
|
923
|
+
]
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
CRITICAL: ALWAYS include "actionType" field in EVERY action (either "direct" or "delegate")
|
|
927
|
+
|
|
928
|
+
Available actions:
|
|
929
|
+
${actionRegistry.generatePromptDocumentation(agent)}
|
|
930
|
+
${hasTeams && agent ? agent.getPeerCapabilitiesAsActions() : ''}
|
|
931
|
+
|
|
932
|
+
${hasTeams ? `\nIMPORTANT: Do NOT nest "intent" inside "data". The "intent" field must be at the top level.` : ''}
|
|
933
|
+
|
|
934
|
+
Data chaining with action outputs:
|
|
935
|
+
- Use \${a1.output.field} to reference the output of action a1
|
|
936
|
+
- Template variables can ONLY be used INSIDE strings
|
|
937
|
+
- NEVER use template variables as direct values: { "count": \${a1.output.length} } ❌ WRONG
|
|
938
|
+
- ALWAYS quote them: { "count": "\${a1.output.length}" } ✅ CORRECT
|
|
939
|
+
- NEVER use the word "undefined" in JSON - use null or a string instead
|
|
940
|
+
|
|
941
|
+
Examples:
|
|
942
|
+
- \${a1.output.count} - Access count field from action a1
|
|
943
|
+
- \${a2.output.users} - Access users array from action a2
|
|
944
|
+
- \${a3.output.users[0].name} - Access nested field
|
|
945
|
+
- After action a5 executes, you can reference \${a5.output} in subsequent actions
|
|
946
|
+
|
|
947
|
+
CRITICAL: When instructions say "Do NOT add print actions", follow that EXACTLY - only generate the actions listed in the steps.
|
|
948
|
+
When using "return" actions with data containing template variables, do NOT add intermediate print actions - they will break the data chain.
|
|
949
|
+
|
|
950
|
+
REMEMBER: Include print actions for ALL output the user should see, UNLESS the instructions explicitly say not to. Return valid, parseable JSON only.`;
|
|
951
|
+
|
|
952
|
+
// Use fastest model for delegated work or short playbooks
|
|
953
|
+
const model = fromDelegation || promptLength < 500 ? 'gpt-4o-mini' : this.model;
|
|
954
|
+
|
|
955
|
+
if (process.env.KOI_DEBUG_LLM) {
|
|
956
|
+
const agentInfo = agent ? ` | Agent: ${agent.name}` : '';
|
|
957
|
+
console.error('─'.repeat(80));
|
|
958
|
+
console.error(`[LLM Debug] executeOpenAIStreaming - Model: ${model}${agentInfo}`);
|
|
959
|
+
console.error('System Prompt:');
|
|
960
|
+
console.error(formatPromptForDebug(systemPrompt));
|
|
961
|
+
console.error('============');
|
|
962
|
+
console.error('User Prompt:');
|
|
963
|
+
console.error('============');
|
|
964
|
+
console.error(formatPromptForDebug(prompt));
|
|
965
|
+
console.error('─'.repeat(80));
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
// Create streaming completion
|
|
969
|
+
const stream = await this.openai.chat.completions.create({
|
|
970
|
+
model,
|
|
971
|
+
messages: [
|
|
972
|
+
{ role: 'system', content: systemPrompt },
|
|
973
|
+
{ role: 'user', content: prompt }
|
|
974
|
+
],
|
|
975
|
+
temperature: 0,
|
|
976
|
+
max_tokens: this.maxTokens,
|
|
977
|
+
stream: true, // Enable streaming
|
|
978
|
+
response_format: { type: "json_object" }
|
|
979
|
+
});
|
|
980
|
+
|
|
981
|
+
// Use incremental parser
|
|
982
|
+
const parser = new IncrementalJSONParser();
|
|
983
|
+
let fullContent = '';
|
|
984
|
+
let streamingStarted = false;
|
|
985
|
+
|
|
986
|
+
if (process.env.KOI_DEBUG_LLM) {
|
|
987
|
+
console.error('[LLM Debug] Starting stream processing...');
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
// Cola de acciones y ejecutor en paralelo
|
|
991
|
+
const actionQueue = [];
|
|
992
|
+
let streamFinished = false;
|
|
993
|
+
let processingError = null;
|
|
994
|
+
let isExecuting = false;
|
|
995
|
+
|
|
996
|
+
// Worker que ejecuta acciones de la cola EN ORDEN (respeta dependencias)
|
|
997
|
+
const processQueue = async () => {
|
|
998
|
+
while (!streamFinished || actionQueue.length > 0) {
|
|
999
|
+
// Esperar si no hay acciones
|
|
1000
|
+
if (actionQueue.length === 0) {
|
|
1001
|
+
await new Promise(resolve => setTimeout(resolve, 10));
|
|
1002
|
+
continue;
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
const action = actionQueue.shift();
|
|
1006
|
+
if (!action) continue;
|
|
1007
|
+
|
|
1008
|
+
// Ejecutar acción EN ORDEN (await para respetar dependencias entre a1, a2, etc.)
|
|
1009
|
+
try {
|
|
1010
|
+
isExecuting = true;
|
|
1011
|
+
await onAction(action);
|
|
1012
|
+
} catch (error) {
|
|
1013
|
+
console.error('[LLM] Error executing action:', error.message);
|
|
1014
|
+
processingError = error;
|
|
1015
|
+
break; // Abortar procesamiento en caso de error
|
|
1016
|
+
} finally {
|
|
1017
|
+
isExecuting = false;
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
};
|
|
1021
|
+
|
|
1022
|
+
// Iniciar el worker de procesamiento si hay onAction
|
|
1023
|
+
const processingPromise = onAction ? processQueue() : null;
|
|
1024
|
+
|
|
1025
|
+
// Process stream
|
|
1026
|
+
try {
|
|
1027
|
+
for await (const chunk of stream) {
|
|
1028
|
+
const delta = chunk.choices[0]?.delta?.content || '';
|
|
1029
|
+
if (delta) {
|
|
1030
|
+
fullContent += delta;
|
|
1031
|
+
|
|
1032
|
+
// Show single-line "Receiving response..." with animation
|
|
1033
|
+
if (process.env.KOI_DEBUG_LLM && !streamingStarted) {
|
|
1034
|
+
streamingStarted = true;
|
|
1035
|
+
cliLogger.planning('Receiving response');
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
// Feed to parser and get any complete actions
|
|
1039
|
+
const actions = parser.feed(delta);
|
|
1040
|
+
|
|
1041
|
+
// Añadir acciones a la cola (no ejecutar directamente)
|
|
1042
|
+
if (onAction && actions.length > 0) {
|
|
1043
|
+
if (process.env.KOI_DEBUG_LLM) {
|
|
1044
|
+
console.error(`\n[LLM Debug] 🚀 Found ${actions.length} complete action(s) - adding to queue (queue size: ${actionQueue.length + actions.length})`);
|
|
1045
|
+
}
|
|
1046
|
+
actionQueue.push(...actions);
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
// Si hubo error en el procesamiento, abortar
|
|
1050
|
+
if (processingError) {
|
|
1051
|
+
throw processingError;
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
// Marcar stream como finalizado
|
|
1057
|
+
streamFinished = true;
|
|
1058
|
+
|
|
1059
|
+
// Clear streaming indicator
|
|
1060
|
+
if (process.env.KOI_DEBUG_LLM && streamingStarted) {
|
|
1061
|
+
cliLogger.clear();
|
|
1062
|
+
}
|
|
1063
|
+
} catch (error) {
|
|
1064
|
+
streamFinished = true;
|
|
1065
|
+
if (streamingStarted) {
|
|
1066
|
+
cliLogger.clear();
|
|
1067
|
+
}
|
|
1068
|
+
console.error('[LLM] Stream processing error:', error.message);
|
|
1069
|
+
if (process.env.KOI_DEBUG_LLM) {
|
|
1070
|
+
console.error(error.stack);
|
|
1071
|
+
}
|
|
1072
|
+
throw error;
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
// Finalize parser to catch any remaining actions
|
|
1076
|
+
const finalActions = parser.finalize();
|
|
1077
|
+
if (onAction && finalActions.length > 0) {
|
|
1078
|
+
actionQueue.push(...finalActions);
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
// Esperar a que se procesen todas las acciones en la cola
|
|
1082
|
+
if (processingPromise) {
|
|
1083
|
+
await processingPromise;
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
// Esperar a que termine la acción actual si está ejecutándose
|
|
1087
|
+
while (isExecuting) {
|
|
1088
|
+
await new Promise(resolve => setTimeout(resolve, 10));
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
// Si hubo error durante el procesamiento, lanzarlo ahora
|
|
1092
|
+
if (processingError) {
|
|
1093
|
+
throw processingError;
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
if (process.env.KOI_DEBUG_LLM) {
|
|
1097
|
+
console.error(`[LLM Debug] executeOpenAIStreaming Complete (${fullContent.length} chars)`);
|
|
1098
|
+
console.error('─'.repeat(80));
|
|
1099
|
+
console.error('[LLM Debug] Response:');
|
|
1100
|
+
// Format each line with < prefix and gray color
|
|
1101
|
+
const lines = fullContent.split('\n');
|
|
1102
|
+
for (const line of lines) {
|
|
1103
|
+
console.error(`< \x1b[90m${line}\x1b[0m`);
|
|
1104
|
+
}
|
|
1105
|
+
console.error('─'.repeat(80));
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
return fullContent;
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
/**
|
|
1112
|
+
* Execute Anthropic call with streaming and incremental action execution
|
|
1113
|
+
* @param {string} prompt - The prompt to send
|
|
1114
|
+
* @param {Object} agent - Agent instance
|
|
1115
|
+
* @param {Function} onAction - Callback called for each complete action: (action) => void
|
|
1116
|
+
* @returns {Promise<string>} - Final response content
|
|
1117
|
+
*/
|
|
1118
|
+
async executeAnthropicStreaming(prompt, agent = null, onAction = null) {
|
|
1119
|
+
if (!process.env.ANTHROPIC_API_KEY) {
|
|
1120
|
+
throw new Error('ANTHROPIC_API_KEY not set in environment');
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
// Check if agent has teams for delegation
|
|
1124
|
+
const hasTeams = agent && agent.usesTeams && agent.usesTeams.length > 0;
|
|
1125
|
+
|
|
1126
|
+
const systemPrompt = `You are a Koi agent executor. Your job is to convert user instructions into a precise sequence of executable actions.
|
|
1127
|
+
|
|
1128
|
+
CRITICAL RULES:
|
|
1129
|
+
1. Execute EVERY instruction in the user's request - do not skip any steps
|
|
1130
|
+
2. Return ONLY raw JSON - NO markdown, NO wrapping, NO "result" field
|
|
1131
|
+
3. Follow the EXACT order of instructions given by the user
|
|
1132
|
+
4. Use "print" actions to display ALL requested output to the user
|
|
1133
|
+
5. ALWAYS use valid JSON - all values must be proper JSON types (strings, numbers, objects, arrays, booleans, null)
|
|
1134
|
+
6. EFFICIENCY: Group consecutive print actions into a single print using \\n for line breaks
|
|
1135
|
+
- WRONG: Three separate prints for header lines
|
|
1136
|
+
- RIGHT: One print with "Line1\\nLine2\\nLine3"
|
|
1137
|
+
6b. EFFICIENCY - Batch Operations: When performing the same operation on multiple items, check if a batch/plural version exists:
|
|
1138
|
+
- Look for plural intent names in available delegation actions: createAllUser/createAllUsers (batch) vs createUser (single)
|
|
1139
|
+
- ❌ WRONG: Six separate createUser calls for 6 users
|
|
1140
|
+
- ✅ RIGHT: One createAllUser call with array of all 6 users: { "actionType": "delegate", "intent": "createAllUser", "data": { "users": [{name: "Alice", id: "001", ...}, {name: "Bob", id: "002", ...}, ...] } }
|
|
1141
|
+
- Apply this principle to ANY repeated operation where a batch version exists
|
|
1142
|
+
- Benefits: Fewer network calls, better performance, cleaner action sequences
|
|
1143
|
+
7. ACTION IDs (OPTIONAL): Use "id" field ONLY when you need to reference the result later
|
|
1144
|
+
- Add "id": "a1" only if you'll use \${a1.output} in a future action
|
|
1145
|
+
- Actions without "id" won't save their output (use for print, one-time actions)
|
|
1146
|
+
- Sequential IDs: a1, a2, a3, ... (only for actions that need saving)
|
|
1147
|
+
- Example: { "id": "a1", "intent": "getUser" } → later use \${a1.output.name}
|
|
1148
|
+
8. CRITICAL - DATE/AGE CALCULATIONS & TEXT GENERATION: If playbook contains text templates with {x}, {age}, {días}, {dd/mm/yyyy} or any placeholder:
|
|
1149
|
+
- NEVER generate template expressions with Date arithmetic
|
|
1150
|
+
- MANDATORY: Use format action with the data array
|
|
1151
|
+
- CRITICAL: Copy the COMPLETE template from playbook to format action's "instruction" - preserve ALL details:
|
|
1152
|
+
* Keep ALL conditional logic (e.g., "Estimado o Estimada si es chica, deducelo por el nombre")
|
|
1153
|
+
* Preserve ALL line breaks/spacing (use \n in instruction string for each line break in template)
|
|
1154
|
+
* Keep original language and exact wording
|
|
1155
|
+
* Don't simplify, paraphrase, or omit any part of the template
|
|
1156
|
+
- ❌ ABSOLUTELY WRONG: print with "\${2023 - new Date(birthdate).getFullYear()}" or any Date calculations
|
|
1157
|
+
- ❌ WRONG: Simplifying template from "Estimado (o Estimada si es chica) <nombre>,\n\nComo sabemos..." to "Estimado {name}, Como sabemos..."
|
|
1158
|
+
- ✅ RIGHT: { "id": "formatted", "intent": "format", "data": "\${usersArray}", "instruction": "For each user write:\n\nEstimado (o Estimada si es chica, deducelo por el nombre) {name},\n\nComo sabemos que usted tiene {age} años, le queremos dar la enhorabuena!\n\nAtentamente,\nLa empresa!!\n\nSeparate emails with blank line" }, then print "\${formatted.output.formatted}"
|
|
1159
|
+
9. NEVER use .map() or arrow functions with nested template literals in template variables:
|
|
1160
|
+
- ❌ ABSOLUTELY WRONG: \${array.map(item => \`text \${item.field}\`).join('\\n')} (nested templates CANNOT be evaluated)
|
|
1161
|
+
- ❌ WRONG: print with "\${users.map(u => \`| \${u.name} | \${u.age} |\`).join('\\n')}" (will print literal template string)
|
|
1162
|
+
- ✅ MANDATORY: Use format action for ANY iteration over arrays
|
|
1163
|
+
- For markdown tables: { "id": "aX", "intent": "format", "data": "\${arrayId.output.users}", "instruction": "Generate markdown table with columns: Sr/Sra (deduce from name), Name, Age. Include header row with | Sr/Sra | Name | Age | and separator |--------|------|-----|" }
|
|
1164
|
+
- For lists: { "id": "aX", "intent": "format", "data": "\${arrayId.output.items}", "instruction": "Format each item as: - {name}: {description}" }
|
|
1165
|
+
- Then print: { "intent": "print", "message": "\${aX.output.formatted}" }
|
|
1166
|
+
|
|
1167
|
+
RESPONSE FORMAT (ALWAYS use this):
|
|
1168
|
+
{
|
|
1169
|
+
"actions": [
|
|
1170
|
+
{ "actionType": "direct", "intent": "print", "message": "Display this to user" },
|
|
1171
|
+
{ "actionType": "direct", "intent": "return", "data": {...} }
|
|
1172
|
+
]
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
CORRECT EXAMPLES:
|
|
1176
|
+
|
|
1177
|
+
Example 1 - NEVER hardcode dynamic values (CRITICAL - Follow Rule #4):
|
|
1178
|
+
User prompt: "Create 2 users, then show 'X users created' where X is the count"
|
|
1179
|
+
❌ WRONG - Hardcoded count:
|
|
1180
|
+
{ "actionType": "direct", "intent": "print", "message": "✅ 2 users created" }
|
|
1181
|
+
|
|
1182
|
+
✅ RIGHT - Dynamic count:
|
|
1183
|
+
{ "id": "a1", "actionType": "delegate", "intent": "createUser", "data": {...} },
|
|
1184
|
+
{ "id": "a2", "actionType": "delegate", "intent": "createUser", "data": {...} },
|
|
1185
|
+
{ "actionType": "direct", "intent": "print", "message": "✅ \${a1.output.success && a2.output.success ? 2 : (a1.output.success || a2.output.success ? 1 : 0)} users created" }
|
|
1186
|
+
|
|
1187
|
+
Example 2 - Extracting names from natural language (CRITICAL - Follow Rule #6):
|
|
1188
|
+
User prompt: "Create Alice: id=001, age=30, email=alice@example.com"
|
|
1189
|
+
❌ WRONG - Missing name: { "data": { "id": "001", "age": 30, "email": "alice@example.com" } }
|
|
1190
|
+
✅ RIGHT - Include name: { "data": { "name": "Alice", "id": "001", "age": 30, "email": "alice@example.com" } }
|
|
1191
|
+
|
|
1192
|
+
Example 3 - Delegate with ID:
|
|
1193
|
+
{
|
|
1194
|
+
"actions": [
|
|
1195
|
+
{ "id": "a1", "actionType": "delegate", "intent": "getUser", "data": { "id": "001" } },
|
|
1196
|
+
{ "actionType": "direct", "intent": "print", "message": "User: \${a1.output.name}, age \${a1.output.age}" }
|
|
1197
|
+
]
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
Example 4 - Multiple actions with IDs:
|
|
1201
|
+
{
|
|
1202
|
+
"actions": [
|
|
1203
|
+
{ "id": "a1", "actionType": "delegate", "intent": "listUsers" },
|
|
1204
|
+
{ "actionType": "direct", "intent": "print", "message": "Found \${a1.output.count} users" },
|
|
1205
|
+
{ "id": "a2", "actionType": "delegate", "intent": "getUser", "data": { "id": "001" } },
|
|
1206
|
+
{ "actionType": "direct", "intent": "print", "message": "First user: \${a2.output.name}" }
|
|
1207
|
+
]
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
Example 5 - Registry operations with IDs:
|
|
1211
|
+
{
|
|
1212
|
+
"actions": [
|
|
1213
|
+
{ "id": "a1", "actionType": "direct", "intent": "registry_get", "key": "user:001" },
|
|
1214
|
+
{ "actionType": "direct", "intent": "print", "message": "Name: \${a1.output.value.name}" }
|
|
1215
|
+
]
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
Example 6 - Without IDs (when results aren't needed):
|
|
1219
|
+
{
|
|
1220
|
+
"actions": [
|
|
1221
|
+
{ "actionType": "direct", "intent": "print", "message": "Hello" },
|
|
1222
|
+
{ "actionType": "delegate", "intent": "deleteUser", "data": { "id": "001" } },
|
|
1223
|
+
{ "actionType": "direct", "intent": "print", "message": "User deleted" }
|
|
1224
|
+
]
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
CRITICAL: ALWAYS include "actionType" field in EVERY action (either "direct" or "delegate")
|
|
1228
|
+
|
|
1229
|
+
Available actions:
|
|
1230
|
+
${actionRegistry.generatePromptDocumentation(agent)}
|
|
1231
|
+
${hasTeams && agent ? agent.getPeerCapabilitiesAsActions() : ''}
|
|
1232
|
+
|
|
1233
|
+
${hasTeams ? `\nIMPORTANT: Do NOT nest "intent" inside "data". The "intent" field must be at the top level.` : ''}
|
|
1234
|
+
|
|
1235
|
+
Data chaining with action outputs:
|
|
1236
|
+
- Use \${a1.output.field} to reference the output of action a1
|
|
1237
|
+
- Template variables can ONLY be used INSIDE strings
|
|
1238
|
+
- NEVER use template variables as direct values: { "count": \${a1.output.length} } ❌ WRONG
|
|
1239
|
+
- ALWAYS quote them: { "count": "\${a1.output.length}" } ✅ CORRECT
|
|
1240
|
+
- NEVER use the word "undefined" in JSON - use null or a string instead
|
|
1241
|
+
|
|
1242
|
+
Examples:
|
|
1243
|
+
- \${a1.output.count} - Access count field from action a1
|
|
1244
|
+
- \${a2.output.users} - Access users array from action a2
|
|
1245
|
+
- \${a3.output.users[0].name} - Access nested field
|
|
1246
|
+
- After action a5 executes, you can reference \${a5.output} in subsequent actions
|
|
1247
|
+
|
|
1248
|
+
CRITICAL: When instructions say "Do NOT add print actions", follow that EXACTLY - only generate the actions listed in the steps.
|
|
1249
|
+
When using "return" actions with data containing template variables, do NOT add intermediate print actions - they will break the data chain.
|
|
1250
|
+
|
|
1251
|
+
REMEMBER: Include print actions for ALL output the user should see, UNLESS the instructions explicitly say not to. Return valid, parseable JSON only.`;
|
|
1252
|
+
|
|
1253
|
+
if (process.env.KOI_DEBUG_LLM) {
|
|
1254
|
+
const agentInfo = agent ? ` | Agent: ${agent.name}` : '';
|
|
1255
|
+
console.error('─'.repeat(80));
|
|
1256
|
+
console.error(`[LLM Debug] executeAnthropicStreaming - Model: ${this.model}${agentInfo}`);
|
|
1257
|
+
console.error('System Prompt:');
|
|
1258
|
+
console.error(formatPromptForDebug(systemPrompt));
|
|
1259
|
+
console.error('============');
|
|
1260
|
+
console.error('User Prompt:');
|
|
1261
|
+
console.error('============');
|
|
1262
|
+
console.error(formatPromptForDebug(prompt));
|
|
1263
|
+
console.error('─'.repeat(80));
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
// Create streaming message
|
|
1267
|
+
const stream = await this.anthropic.messages.stream({
|
|
1268
|
+
model: this.model,
|
|
1269
|
+
max_tokens: this.maxTokens,
|
|
1270
|
+
temperature: 0,
|
|
1271
|
+
system: systemPrompt,
|
|
1272
|
+
messages: [{ role: 'user', content: prompt }]
|
|
1273
|
+
});
|
|
1274
|
+
|
|
1275
|
+
// Use incremental parser
|
|
1276
|
+
const parser = new IncrementalJSONParser();
|
|
1277
|
+
let fullContent = '';
|
|
1278
|
+
let streamingStarted = false;
|
|
1279
|
+
|
|
1280
|
+
// Process stream
|
|
1281
|
+
stream.on('text', (delta) => {
|
|
1282
|
+
fullContent += delta;
|
|
1283
|
+
|
|
1284
|
+
// Show single-line "Receiving response..." with animation
|
|
1285
|
+
if (process.env.KOI_DEBUG_LLM && !streamingStarted) {
|
|
1286
|
+
streamingStarted = true;
|
|
1287
|
+
cliLogger.planning('Receiving response');
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
// Feed to parser and get any complete actions
|
|
1291
|
+
const actions = parser.feed(delta);
|
|
1292
|
+
|
|
1293
|
+
// Execute each complete action immediately
|
|
1294
|
+
if (onAction && actions.length > 0) {
|
|
1295
|
+
if (process.env.KOI_DEBUG_LLM) {
|
|
1296
|
+
console.error(`\n[LLM Debug] 🚀 Found ${actions.length} complete action(s) - executing immediately!`);
|
|
1297
|
+
}
|
|
1298
|
+
for (const action of actions) {
|
|
1299
|
+
onAction(action).catch(err => {
|
|
1300
|
+
console.error(`[Stream] Error executing action: ${err.message}`);
|
|
1301
|
+
});
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1304
|
+
});
|
|
1305
|
+
|
|
1306
|
+
// Wait for stream to complete
|
|
1307
|
+
const message = await stream.finalMessage();
|
|
1308
|
+
|
|
1309
|
+
// Clear streaming indicator
|
|
1310
|
+
if (process.env.KOI_DEBUG_LLM && streamingStarted) {
|
|
1311
|
+
cliLogger.clear();
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
// Finalize parser to catch any remaining actions
|
|
1315
|
+
const finalActions = parser.finalize();
|
|
1316
|
+
if (onAction && finalActions.length > 0) {
|
|
1317
|
+
for (const action of finalActions) {
|
|
1318
|
+
await onAction(action);
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
if (process.env.KOI_DEBUG_LLM) {
|
|
1323
|
+
console.error(`[LLM Debug] executeAnthropicStreaming Complete (${fullContent.length} chars)`);
|
|
1324
|
+
console.error('─'.repeat(80));
|
|
1325
|
+
console.error('[LLM Debug] Response:');
|
|
1326
|
+
// Format each line with < prefix and gray color
|
|
1327
|
+
const lines = fullContent.split('\n');
|
|
1328
|
+
for (const line of lines) {
|
|
1329
|
+
console.error(`< \x1b[90m${line}\x1b[0m`);
|
|
1330
|
+
}
|
|
1331
|
+
console.error('─'.repeat(80));
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1334
|
+
return fullContent;
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1337
|
+
/**
|
|
1338
|
+
* Generate embeddings for semantic search
|
|
1339
|
+
* Uses OpenAI's text-embedding-3-small for fast, cheap embeddings
|
|
1340
|
+
*/
|
|
1341
|
+
async getEmbedding(text) {
|
|
1342
|
+
// Validate input
|
|
1343
|
+
if (!text || typeof text !== 'string' || text.trim() === '') {
|
|
1344
|
+
throw new Error('getEmbedding requires non-empty text input');
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
if (this.provider === 'openai' || this.provider === 'anthropic') {
|
|
1348
|
+
// Always use OpenAI for embeddings (Anthropic doesn't have embeddings API)
|
|
1349
|
+
if (!process.env.OPENAI_API_KEY) {
|
|
1350
|
+
throw new Error('OPENAI_API_KEY required for embeddings');
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
if (!this.openai) {
|
|
1354
|
+
this.openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1357
|
+
try {
|
|
1358
|
+
const response = await this.openai.embeddings.create({
|
|
1359
|
+
model: 'text-embedding-3-small',
|
|
1360
|
+
input: text.trim()
|
|
1361
|
+
});
|
|
1362
|
+
|
|
1363
|
+
return response.data[0].embedding;
|
|
1364
|
+
} catch (error) {
|
|
1365
|
+
console.error(`[LLM] Error generating embedding:`, error.message);
|
|
1366
|
+
throw error;
|
|
1367
|
+
}
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
throw new Error(`Embeddings not supported for provider: ${this.provider}`);
|
|
1371
|
+
}
|
|
1372
|
+
}
|