@koi-language/koi 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. package/QUICKSTART.md +89 -0
  2. package/README.md +545 -0
  3. package/examples/actions-demo.koi +177 -0
  4. package/examples/cache-test.koi +29 -0
  5. package/examples/calculator.koi +61 -0
  6. package/examples/clear-registry.js +33 -0
  7. package/examples/clear-registry.koi +30 -0
  8. package/examples/code-introspection-test.koi +149 -0
  9. package/examples/counter.koi +132 -0
  10. package/examples/delegation-test.koi +52 -0
  11. package/examples/directory-import-test.koi +84 -0
  12. package/examples/hello-world-claude.koi +52 -0
  13. package/examples/hello-world.koi +52 -0
  14. package/examples/hello.koi +24 -0
  15. package/examples/mcp-example.koi +70 -0
  16. package/examples/multi-event-handler-test.koi +144 -0
  17. package/examples/new-import-test.koi +89 -0
  18. package/examples/pipeline.koi +162 -0
  19. package/examples/registry-demo.koi +184 -0
  20. package/examples/registry-playbook-demo.koi +162 -0
  21. package/examples/registry-playbook-email-compositor-2.koi +140 -0
  22. package/examples/registry-playbook-email-compositor.koi +140 -0
  23. package/examples/sentiment.koi +90 -0
  24. package/examples/simple.koi +48 -0
  25. package/examples/skill-import-test.koi +76 -0
  26. package/examples/skills/advanced/index.koi +95 -0
  27. package/examples/skills/math-operations.koi +69 -0
  28. package/examples/skills/string-operations.koi +56 -0
  29. package/examples/task-chaining-demo.koi +244 -0
  30. package/examples/test-await.koi +22 -0
  31. package/examples/test-crypto-sha256.koi +196 -0
  32. package/examples/test-delegation.koi +41 -0
  33. package/examples/test-multi-team-routing.koi +258 -0
  34. package/examples/test-no-handler.koi +35 -0
  35. package/examples/test-npm-import.koi +67 -0
  36. package/examples/test-parse.koi +10 -0
  37. package/examples/test-peers-with-team.koi +59 -0
  38. package/examples/test-permissions-fail.koi +20 -0
  39. package/examples/test-permissions.koi +36 -0
  40. package/examples/test-simple-registry.koi +31 -0
  41. package/examples/test-typescript-import.koi +64 -0
  42. package/examples/test-uses-team-syntax.koi +25 -0
  43. package/examples/test-uses-team.koi +31 -0
  44. package/examples/utils/calculator.test.ts +144 -0
  45. package/examples/utils/calculator.ts +56 -0
  46. package/examples/utils/math-helpers.js +50 -0
  47. package/examples/utils/math-helpers.ts +55 -0
  48. package/examples/web-delegation-demo.koi +165 -0
  49. package/package.json +78 -0
  50. package/src/cli/koi.js +793 -0
  51. package/src/compiler/build-optimizer.js +447 -0
  52. package/src/compiler/cache-manager.js +274 -0
  53. package/src/compiler/import-resolver.js +369 -0
  54. package/src/compiler/parser.js +7542 -0
  55. package/src/compiler/transpiler.js +1105 -0
  56. package/src/compiler/typescript-transpiler.js +148 -0
  57. package/src/grammar/koi.pegjs +767 -0
  58. package/src/runtime/action-registry.js +172 -0
  59. package/src/runtime/actions/call-skill.js +45 -0
  60. package/src/runtime/actions/format.js +115 -0
  61. package/src/runtime/actions/print.js +42 -0
  62. package/src/runtime/actions/registry-delete.js +37 -0
  63. package/src/runtime/actions/registry-get.js +37 -0
  64. package/src/runtime/actions/registry-keys.js +33 -0
  65. package/src/runtime/actions/registry-search.js +34 -0
  66. package/src/runtime/actions/registry-set.js +50 -0
  67. package/src/runtime/actions/return.js +31 -0
  68. package/src/runtime/actions/send-message.js +58 -0
  69. package/src/runtime/actions/update-state.js +36 -0
  70. package/src/runtime/agent.js +1368 -0
  71. package/src/runtime/cli-logger.js +205 -0
  72. package/src/runtime/incremental-json-parser.js +201 -0
  73. package/src/runtime/index.js +33 -0
  74. package/src/runtime/llm-provider.js +1372 -0
  75. package/src/runtime/mcp-client.js +1171 -0
  76. package/src/runtime/planner.js +273 -0
  77. package/src/runtime/registry-backends/keyv-sqlite.js +215 -0
  78. package/src/runtime/registry-backends/local.js +260 -0
  79. package/src/runtime/registry.js +162 -0
  80. package/src/runtime/role.js +14 -0
  81. package/src/runtime/router.js +395 -0
  82. package/src/runtime/runtime.js +113 -0
  83. package/src/runtime/skill-selector.js +173 -0
  84. package/src/runtime/skill.js +25 -0
  85. package/src/runtime/team.js +162 -0
@@ -0,0 +1,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
+ }