@koi-language/koi 1.0.6 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (113) hide show
  1. package/README.md +4 -125
  2. package/examples/.build/agent-dialogue.ts +138 -0
  3. package/examples/.build/agent-dialogue.ts.map +1 -0
  4. package/examples/.build/chess.ts +77 -0
  5. package/examples/.build/chess.ts.map +1 -0
  6. package/examples/.build/delegation-test.ts +140 -0
  7. package/examples/.build/delegation-test.ts.map +1 -0
  8. package/examples/.build/dialog-demo.ts +77 -0
  9. package/examples/.build/dialog-demo.ts.map +1 -0
  10. package/examples/.build/hello-world.ts +77 -0
  11. package/examples/.build/hello-world.ts.map +1 -0
  12. package/examples/.build/lover-dialog-demo.ts +77 -0
  13. package/examples/.build/lover-dialog-demo.ts.map +1 -0
  14. package/examples/.build/package.json +3 -0
  15. package/examples/.build/registry-interactive-demo.ts +202 -0
  16. package/examples/.build/registry-interactive-demo.ts.map +1 -0
  17. package/examples/.build/registry-playbook-demo.ts +201 -0
  18. package/examples/.build/registry-playbook-demo.ts.map +1 -0
  19. package/examples/.build/tic-tac-toe.ts +77 -0
  20. package/examples/.build/tic-tac-toe.ts.map +1 -0
  21. package/examples/actions-demo.koi +8 -9
  22. package/examples/activists-dialogue.koi +75 -0
  23. package/examples/agent-dialogue.koi +66 -0
  24. package/examples/chess.koi +19 -0
  25. package/examples/counter.koi +20 -69
  26. package/examples/delegation-test.koi +16 -18
  27. package/examples/dialog-demo.koi +20 -0
  28. package/examples/hello-world.koi +7 -43
  29. package/examples/mcp-stdio-demo.koi +29 -0
  30. package/examples/memory-test.koi +49 -0
  31. package/examples/mobile-mcp-demo.koi +32 -0
  32. package/examples/multi-event-handler-test.koi +16 -18
  33. package/examples/pipeline.koi +15 -17
  34. package/examples/prompt-demo.koi +20 -0
  35. package/examples/{registry-playbook-email-compositor.koi → registry-interactive-demo.koi} +27 -27
  36. package/examples/registry-playbook-demo.koi +28 -28
  37. package/examples/skill-import-test.koi +7 -9
  38. package/examples/skills/.build/math-operations.ts +1656 -0
  39. package/examples/skills/.build/math-operations.ts.map +1 -0
  40. package/examples/skills/.build/package.json +3 -0
  41. package/examples/skills/.build/string-operations.ts +1643 -0
  42. package/examples/skills/.build/string-operations.ts.map +1 -0
  43. package/examples/skills/advanced/.build/index.ts +3223 -0
  44. package/examples/skills/advanced/.build/index.ts.map +1 -0
  45. package/examples/skills/advanced/.build/package.json +3 -0
  46. package/examples/skills/advanced/index.koi +3 -5
  47. package/examples/skills/math-operations.koi +1 -3
  48. package/examples/skills/string-operations.koi +1 -3
  49. package/examples/tic-tac-toe.koi +19 -0
  50. package/examples/utils/echo-mcp-server.js +141 -0
  51. package/examples/web-delegation-demo.koi +15 -17
  52. package/package.json +2 -1
  53. package/src/cli/koi.js +30 -41
  54. package/src/compiler/build-optimizer.js +204 -289
  55. package/src/compiler/cache-manager.js +1 -1
  56. package/src/compiler/import-resolver.js +5 -9
  57. package/src/compiler/parser.js +6072 -3476
  58. package/src/compiler/transpiler.js +346 -38
  59. package/src/grammar/koi.pegjs +302 -62
  60. package/src/runtime/actions/{format.js → call-llm.js} +37 -44
  61. package/src/runtime/actions/call-mcp.js +97 -0
  62. package/src/runtime/actions/if.js +179 -0
  63. package/src/runtime/actions/print.js +3 -1
  64. package/src/runtime/actions/prompt-user.js +75 -0
  65. package/src/runtime/actions/repeat.js +147 -0
  66. package/src/runtime/actions/shell.js +185 -0
  67. package/src/runtime/actions/while.js +205 -0
  68. package/src/runtime/agent.js +592 -178
  69. package/src/runtime/cli-display.js +26 -0
  70. package/src/runtime/cli-input.js +421 -0
  71. package/src/runtime/cli-logger.js +2 -5
  72. package/src/runtime/cli-markdown.js +61 -0
  73. package/src/runtime/cli-select.js +106 -0
  74. package/src/runtime/incremental-json-parser.js +27 -17
  75. package/src/runtime/index.js +1 -0
  76. package/src/runtime/llm-provider.js +1083 -572
  77. package/src/runtime/mcp-registry.js +141 -0
  78. package/src/runtime/mcp-stdio-client.js +334 -0
  79. package/src/runtime/planner.js +1 -1
  80. package/src/runtime/playbook-session.js +259 -0
  81. package/src/runtime/registry-backends/keyv-sqlite.js +1 -1
  82. package/src/runtime/registry-backends/local.js +1 -1
  83. package/src/runtime/router.js +22 -26
  84. package/src/runtime/runtime.js +7 -1
  85. package/examples/cache-test.koi +0 -29
  86. package/examples/calculator.koi +0 -61
  87. package/examples/clear-registry.js +0 -33
  88. package/examples/clear-registry.koi +0 -30
  89. package/examples/code-introspection-test.koi +0 -149
  90. package/examples/directory-import-test.koi +0 -84
  91. package/examples/hello-world-claude.koi +0 -52
  92. package/examples/hello.koi +0 -24
  93. package/examples/mcp-example.koi +0 -70
  94. package/examples/new-import-test.koi +0 -89
  95. package/examples/registry-demo.koi +0 -184
  96. package/examples/registry-playbook-email-compositor-2.koi +0 -140
  97. package/examples/sentiment.koi +0 -90
  98. package/examples/simple.koi +0 -48
  99. package/examples/task-chaining-demo.koi +0 -244
  100. package/examples/test-await.koi +0 -22
  101. package/examples/test-crypto-sha256.koi +0 -196
  102. package/examples/test-delegation.koi +0 -41
  103. package/examples/test-multi-team-routing.koi +0 -258
  104. package/examples/test-no-handler.koi +0 -35
  105. package/examples/test-npm-import.koi +0 -67
  106. package/examples/test-parse.koi +0 -10
  107. package/examples/test-peers-with-team.koi +0 -59
  108. package/examples/test-permissions-fail.koi +0 -20
  109. package/examples/test-permissions.koi +0 -36
  110. package/examples/test-simple-registry.koi +0 -31
  111. package/examples/test-typescript-import.koi +0 -64
  112. package/examples/test-uses-team-syntax.koi +0 -25
  113. package/examples/test-uses-team.koi +0 -31
@@ -22,7 +22,7 @@ function formatPromptForDebug(text) {
22
22
  export class LLMProvider {
23
23
  constructor(config = {}) {
24
24
  this.provider = config.provider || 'openai';
25
- this.model = config.model || 'gpt-4o-mini';
25
+ this.model = config.model;
26
26
  this.temperature = config.temperature ?? 0.1; // Low temperature for deterministic results
27
27
  this.maxTokens = config.max_tokens || 8000; // Increased to avoid truncation of long responses
28
28
 
@@ -43,18 +43,182 @@ export class LLMProvider {
43
43
  throw new Error('ANTHROPIC_API_KEY is required for Anthropic provider');
44
44
  }
45
45
  this.anthropic = new Anthropic({ apiKey });
46
+ } else if (this.provider === 'gemini') {
47
+ const apiKey = process.env.GEMINI_API_KEY;
48
+ if (!apiKey) {
49
+ console.error('\n⚠️ GEMINI_API_KEY not found!');
50
+ console.error(' Set it as environment variable or create a .env file\n');
51
+ throw new Error('GEMINI_API_KEY is required for Gemini provider');
52
+ }
53
+ // Gemini exposes an OpenAI-compatible endpoint — reuse the OpenAI SDK
54
+ this.openai = new OpenAI({
55
+ apiKey,
56
+ baseURL: 'https://generativelanguage.googleapis.com/v1beta/openai/'
57
+ });
58
+ }
59
+ }
60
+
61
+ /**
62
+ * Format text for debug output with gray color
63
+ */
64
+ formatDebugText(text) {
65
+ const lines = text.split('\n');
66
+ return lines.map(line => `> \x1b[90m${line}\x1b[0m`).join('\n');
67
+ }
68
+
69
+ /**
70
+ * Log LLM request (system + user prompts)
71
+ */
72
+ logRequest(model, systemPrompt, userPrompt, context = '') {
73
+ if (process.env.KOI_DEBUG_LLM !== '1') return;
74
+
75
+ console.error('─'.repeat(80));
76
+ console.error(`[LLM Debug] Request - Model: ${model}${context ? ' | ' + context : ''}`);
77
+ console.error('System Prompt:');
78
+ console.error(this.formatDebugText(systemPrompt));
79
+ console.error('============');
80
+ console.error('User Prompt:');
81
+ console.error('============');
82
+ console.error(this.formatDebugText(userPrompt));
83
+ console.error('─'.repeat(80));
84
+ }
85
+
86
+ /**
87
+ * Log LLM response
88
+ */
89
+ logResponse(content, context = '') {
90
+ if (process.env.KOI_DEBUG_LLM !== '1') return;
91
+
92
+ console.error(`\n[LLM Debug] Response${context ? ' - ' + context : ''} (${content.length} chars)`);
93
+ console.error('─'.repeat(80));
94
+
95
+ // Try to format JSON for better readability
96
+ let formattedContent = content;
97
+ try {
98
+ const parsed = JSON.parse(content);
99
+ formattedContent = JSON.stringify(parsed, null, 2);
100
+ } catch (e) {
101
+ // Not JSON, use as is
102
+ }
103
+
104
+ const lines = formattedContent.split('\n');
105
+ for (const line of lines) {
106
+ console.error(`< \x1b[90m${line}\x1b[0m`);
107
+ }
108
+ console.error('─'.repeat(80));
109
+ }
110
+
111
+ /**
112
+ * Log simple message
113
+ */
114
+ logDebug(message) {
115
+ if (process.env.KOI_DEBUG_LLM !== '1') return;
116
+ console.error(`[LLM Debug] ${message}`);
117
+ }
118
+
119
+ /**
120
+ * Log error
121
+ */
122
+ logError(message, error) {
123
+ if (process.env.KOI_DEBUG_LLM !== '1') return;
124
+ console.error(`[LLM Debug] ERROR: ${message}`);
125
+ if (error) {
126
+ console.error(error.stack || error.message);
46
127
  }
47
128
  }
48
129
 
130
+ /**
131
+ * Simple chat completion for build-time tasks (descriptions, summaries).
132
+ * No system prompt injection, no JSON mode, with timeout.
133
+ */
134
+ async simpleChat(prompt, { timeoutMs = 15000 } = {}) {
135
+ const messages = [{ role: 'user', content: prompt }];
136
+
137
+ if (this.provider === 'openai' || this.provider === 'gemini') {
138
+ const controller = new AbortController();
139
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
140
+ try {
141
+ const completion = await this.openai.chat.completions.create(
142
+ this.buildApiParams({
143
+ model: this.model,
144
+ messages,
145
+ temperature: 0.1,
146
+ max_tokens: this.maxTokens || 150
147
+ }),
148
+ { signal: controller.signal }
149
+ );
150
+ return completion.choices[0].message.content?.trim() || '';
151
+ } finally {
152
+ clearTimeout(timer);
153
+ }
154
+ } else if (this.provider === 'anthropic') {
155
+ const message = await this.anthropic.messages.create({
156
+ model: this.model,
157
+ max_tokens: this.maxTokens || 150,
158
+ temperature: 0.1,
159
+ messages
160
+ });
161
+ return message.content[0].text.trim();
162
+ }
163
+ return '';
164
+ }
165
+
166
+ /**
167
+ * Call OpenAI with logging
168
+ * @param {Object} options - { model, messages, temperature, max_tokens, stream, response_format }
169
+ * @param {string} context - Context description for logging
170
+ * @returns {Promise} - OpenAI completion response
171
+ */
172
+ async callOpenAI(options, context = '') {
173
+ const { model, messages, temperature = 0, max_tokens = 4000, stream = false, response_format } = options;
174
+
175
+ // Extract prompts for logging
176
+ const systemPrompt = messages.find(m => m.role === 'system')?.content || '';
177
+ const userPrompt = messages.find(m => m.role === 'user')?.content || '';
178
+
179
+ // Log request
180
+ this.logRequest(model, systemPrompt, userPrompt, context);
181
+
182
+ // Make API call with buildApiParams to handle gpt-5.2
183
+ const completion = await this.openai.chat.completions.create(
184
+ this.buildApiParams({
185
+ model,
186
+ messages,
187
+ temperature,
188
+ max_tokens,
189
+ stream,
190
+ ...(response_format && { response_format })
191
+ })
192
+ );
193
+
194
+ // If not streaming, log response immediately
195
+ if (!stream) {
196
+ const content = completion.choices[0].message.content;
197
+ this.logResponse(content, context);
198
+ }
199
+
200
+ return completion;
201
+ }
202
+
203
+ /**
204
+ * Build API parameters, excluding max_tokens for gpt-5.2
205
+ */
206
+ buildApiParams(baseParams) {
207
+ // gpt-5.2 doesn't accept max_tokens parameter
208
+ if (baseParams.model === 'gpt-5.2') {
209
+ const { max_tokens, ...paramsWithoutMaxTokens } = baseParams;
210
+ return paramsWithoutMaxTokens;
211
+ }
212
+ return baseParams;
213
+ }
214
+
49
215
  async executePlanning(prompt) {
50
- // Simple, fast planning call without all the overhead
51
- // ALWAYS use the fastest model for planning
52
216
  try {
53
217
  let response;
54
218
 
55
219
  if (this.provider === 'openai') {
56
220
  const completion = await this.openai.chat.completions.create({
57
- model: 'gpt-4o-mini', // Force fastest model for planning
221
+ model: 'gpt-5.2', // Force best model for planning
58
222
  messages: [
59
223
  {
60
224
  role: 'system',
@@ -62,8 +226,7 @@ export class LLMProvider {
62
226
  },
63
227
  { role: 'user', content: prompt }
64
228
  ],
65
- temperature: 0, // Use 0 for maximum determinism
66
- max_tokens: 800
229
+ temperature: 0
67
230
  });
68
231
  response = completion.choices[0].message.content.trim();
69
232
  } else if (this.provider === 'anthropic') {
@@ -86,7 +249,7 @@ export class LLMProvider {
86
249
  }
87
250
  }
88
251
 
89
- async executePlaybook(playbook, context = {}, agentName = null, tools = [], agent = null, fromDelegation = false, onAction = null) {
252
+ async executePlaybook(playbook, context = {}, agentName = null, tools = [], agent = null, fromDelegation = false, onAction = null, memory = []) {
90
253
  // Show planning animation while LLM is thinking
91
254
  // Format: [🤖 AgentName] Thinking...
92
255
  const planningPrefix = agentName ? `[🤖 ${agentName}]` : '';
@@ -112,15 +275,22 @@ Respond with ONLY valid JSON.`;
112
275
  if (useStreaming) {
113
276
  // hasTeams should only be true if agent can delegate to others
114
277
  const hasTeams = agent && agent.usesTeams && agent.usesTeams.length > 0;
115
- response = await this.executeOpenAIStreaming(prompt, fromDelegation, hasTeams, playbook.length, agent, onAction);
278
+ response = await this.executeOpenAIStreaming(prompt, fromDelegation, hasTeams, playbook.length, agent, onAction, memory);
116
279
  } else {
117
- response = await this.executeOpenAIWithTools(prompt, tools, agent, fromDelegation, playbook.length);
280
+ response = await this.executeOpenAIWithTools(prompt, tools, agent, fromDelegation, playbook.length, memory);
281
+ }
282
+ } else if (this.provider === 'gemini') {
283
+ if (useStreaming) {
284
+ const hasTeams = agent && agent.usesTeams && agent.usesTeams.length > 0;
285
+ response = await this.executeGeminiStreaming(prompt, hasTeams, agent, onAction, memory);
286
+ } else {
287
+ response = await this.executeGemini(prompt, agent, memory);
118
288
  }
119
289
  } else if (this.provider === 'anthropic') {
120
290
  if (useStreaming) {
121
- response = await this.executeAnthropicStreaming(prompt, agent, onAction);
291
+ response = await this.executeAnthropicStreaming(prompt, agent, onAction, memory);
122
292
  } else {
123
- response = await this.executeAnthropic(prompt, agent);
293
+ response = await this.executeAnthropic(prompt, agent, memory);
124
294
  }
125
295
  } else {
126
296
  throw new Error(`Unknown provider: ${this.provider}`);
@@ -184,7 +354,30 @@ Respond with ONLY valid JSON.`;
184
354
  }
185
355
  }
186
356
 
187
- async executeOpenAI(prompt, fromDelegation = false, hasTeams = false, promptLength = 0, agent = null) {
357
+ /**
358
+ * Generate MCP tools documentation for plan-then-execute system prompts.
359
+ * @param {Agent} agent
360
+ * @returns {string}
361
+ */
362
+ _getMCPToolsDoc(agent) {
363
+ if (!agent?.usesMCPNames?.length) return '';
364
+
365
+ const mcpSummaries = agent.getMCPToolsSummary?.() || [];
366
+ if (mcpSummaries.length === 0) return '';
367
+
368
+ let doc = '\n\nMCP Server tools (use call_mcp action):\n';
369
+ for (const mcp of mcpSummaries) {
370
+ for (const tool of mcp.tools) {
371
+ const inputDesc = tool.inputSchema?.properties
372
+ ? Object.keys(tool.inputSchema.properties).map(k => `"${k}": ...`).join(', ')
373
+ : '...';
374
+ doc += `- { "actionType": "direct", "intent": "call_mcp", "mcp": "${mcp.name}", "tool": "${tool.name}", "input": { ${inputDesc} } } - ${tool.description || tool.name}\n`;
375
+ }
376
+ }
377
+ return doc;
378
+ }
379
+
380
+ async executeOpenAI(prompt, fromDelegation = false, hasTeams = false, promptLength = 0, agent = null, memory = []) {
188
381
  if (!process.env.OPENAI_API_KEY) {
189
382
  throw new Error('OPENAI_API_KEY not set in environment');
190
383
  }
@@ -205,170 +398,45 @@ CRITICAL: When delegating work that involves MULTIPLE items (e.g., "create these
205
398
  - NEVER group multiple items into one action unless the handler explicitly expects an array`
206
399
  : '';
207
400
 
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
- }
401
+ const mcpToolsDoc = this._getMCPToolsDoc(agent);
324
402
 
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
- }
403
+ const systemPrompt = `Convert the following instructions into executable JSON actions.
334
404
 
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
- }
405
+ OUTPUT: { "actions": [...] }
342
406
 
343
- Example 6 - Without IDs (when results aren't needed):
344
- {
407
+ CRITICAL RULES:
408
+ 1. call_llm: ONLY when playbook says "random", "relacionado", "based on", "adapted", "generate question". If playbook can generate content directly, do NOT use call_llm.
409
+ 2. Loops: "hasta que se despida" → while with llm_eval condition
410
+ 3. IDs: Actions MUST have "id" if output will be referenced via \${id.output}
411
+ 4. Template variables ONLY in strings: "text \${var}" not \${var}
412
+ 5. Group consecutive prints with \\n
413
+
414
+ WHILE LOOP EXAMPLE:
415
+ { "id": "name", "intent": "prompt_user", "question": "¿Cuál es tu nombre?" },
416
+ { "intent": "registry_set", "key": "last", "value": "\${name.output.answer}" },
417
+ { "intent": "while",
418
+ "condition": { "llm_eval": true, "instruction": "¿Continuar?", "data": "\${response.output.answer}" },
345
419
  "actions": [
346
- { "actionType": "direct", "intent": "print", "message": "Hello" },
347
- { "actionType": "delegate", "intent": "deleteUser", "data": { "id": "001" } },
348
- { "actionType": "direct", "intent": "print", "message": "User deleted" }
420
+ { "id": "prev", "intent": "registry_get", "key": "last" },
421
+ { "id": "question", "intent": "call_llm", "data": {"answer":"\${prev.output.value}"}, "instruction": "Generate related question" },
422
+ { "id": "response", "intent": "prompt_user", "question": "\${question.output.result}" },
423
+ { "intent": "registry_set", "key": "last", "value": "\${response.output.answer}" }
349
424
  ]
350
425
  }
351
-
352
- CRITICAL: ALWAYS include "actionType" field in EVERY action (either "direct" or "delegate")
426
+ CRITICAL: condition.data uses ID from prompt_user INSIDE loop ("response"), NOT from outside ("name")
353
427
 
354
428
  Available actions:
355
429
  ${actionRegistry.generatePromptDocumentation(agent)}
356
- ${hasTeams && agent ? agent.getPeerCapabilitiesAsActions() : ''}
430
+ ${hasTeams && agent ? await agent.getPeerCapabilitiesAsActions() : ''}
431
+ ${mcpToolsDoc}
357
432
 
358
433
  ${hasTeams ? `\nIMPORTANT: Do NOT nest "intent" inside "data". The "intent" field must be at the top level.` : ''}
359
434
 
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
435
+ Data chaining:
436
+ - Reference action outputs: \${actionId.output.field}
437
+ - Template variables ONLY in strings: { "count": "\${user.output.length}" } ✅ NOT { "count": \${user.output.length} } ❌
438
+ - Use descriptive IDs: "user", "question", "response", NOT "a1", "a2", "a3"
439
+ - Examples: \${user.output.name}, \${question.output.result}, \${response.output.answer}
372
440
 
373
441
  CRITICAL: When instructions say "Do NOT add print actions", follow that EXACTLY - only generate the actions listed in the steps.
374
442
  When using "return" actions with data containing template variables, do NOT add intermediate print actions - they will break the data chain.
@@ -376,8 +444,8 @@ When using "return" actions with data containing template variables, do NOT add
376
444
  REMEMBER: Include print actions for ALL output the user should see, UNLESS the instructions explicitly say not to. Return valid, parseable JSON only.`;
377
445
 
378
446
 
379
- // Use fastest model for delegated work or short playbooks
380
- const model = fromDelegation || promptLength < 500 ? 'gpt-4o-mini' : this.model;
447
+ // Use agent's configured model
448
+ const model = this.model;
381
449
 
382
450
  if (process.env.KOI_DEBUG_LLM) {
383
451
  const agentInfo = agent ? ` | Agent: ${agent.name}` : '';
@@ -392,22 +460,25 @@ REMEMBER: Include print actions for ALL output the user should see, UNLESS the i
392
460
  console.error('─'.repeat(80));
393
461
  }
394
462
 
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
- });
463
+ const completion = await this.openai.chat.completions.create(
464
+ this.buildApiParams({
465
+ model,
466
+ messages: [
467
+ {
468
+ role: 'system',
469
+ content: systemPrompt
470
+ },
471
+ ...memory,
472
+ {
473
+ role: 'user',
474
+ content: prompt
475
+ }
476
+ ],
477
+ temperature: 0, // Always use 0 for maximum determinism
478
+ max_tokens: this.maxTokens,
479
+ response_format: { type: "json_object" } // Force valid JSON responses
480
+ })
481
+ );
411
482
 
412
483
  const content = completion.choices[0].message.content?.trim() || '';
413
484
 
@@ -424,7 +495,7 @@ REMEMBER: Include print actions for ALL output the user should see, UNLESS the i
424
495
  return content;
425
496
  }
426
497
 
427
- async executeOpenAIWithTools(prompt, tools = [], agent = null, fromDelegation = false, promptLength = 0) {
498
+ async executeOpenAIWithTools(prompt, tools = [], agent = null, fromDelegation = false, promptLength = 0, memory = []) {
428
499
  if (!process.env.OPENAI_API_KEY) {
429
500
  throw new Error('OPENAI_API_KEY not set in environment');
430
501
  }
@@ -434,7 +505,7 @@ REMEMBER: Include print actions for ALL output the user should see, UNLESS the i
434
505
  // hasTeams should only be true if agent can delegate to others (uses teams as a client)
435
506
  // NOT if agent is just a member of a team (has peers)
436
507
  const hasTeams = agent && agent.usesTeams && agent.usesTeams.length > 0;
437
- return await this.executeOpenAI(prompt, fromDelegation, hasTeams, promptLength, agent);
508
+ return await this.executeOpenAI(prompt, fromDelegation, hasTeams, promptLength, agent, memory);
438
509
  }
439
510
 
440
511
  // Convert tools to OpenAI format
@@ -546,11 +617,12 @@ You respond with valid JSON only. No markdown, no code blocks, no explanations.`
546
617
 
547
618
  const messages = [
548
619
  { role: 'system', content: systemPrompt },
620
+ ...memory,
549
621
  { role: 'user', content: prompt }
550
622
  ];
551
623
 
552
- // Use fastest model for delegated work or short prompts
553
- const model = fromDelegation || promptLength < 500 ? 'gpt-4o-mini' : this.model;
624
+ // Use agent's configured model
625
+ const model = this.model;
554
626
 
555
627
  if (process.env.KOI_DEBUG_LLM) {
556
628
  const agentInfo = agent ? ` | Agent: ${agent.name}` : '';
@@ -566,15 +638,17 @@ You respond with valid JSON only. No markdown, no code blocks, no explanations.`
566
638
  }
567
639
 
568
640
  // 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
- });
641
+ let completion = await this.openai.chat.completions.create(
642
+ this.buildApiParams({
643
+ model,
644
+ messages,
645
+ tools: openAITools,
646
+ tool_choice: 'auto',
647
+ temperature: 0, // Always use 0 for maximum determinism
648
+ max_tokens: this.maxTokens,
649
+ response_format: { type: "json_object" } // Force valid JSON responses
650
+ })
651
+ );
578
652
 
579
653
  let message = completion.choices[0].message;
580
654
 
@@ -629,13 +703,15 @@ You respond with valid JSON only. No markdown, no code blocks, no explanations.`
629
703
  }
630
704
 
631
705
  // 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
- });
706
+ completion = await this.openai.chat.completions.create(
707
+ this.buildApiParams({
708
+ model, // Use same model as initial call
709
+ messages,
710
+ temperature: 0, // Always use 0 for maximum determinism
711
+ max_tokens: this.maxTokens,
712
+ response_format: { type: "json_object" } // Force valid JSON responses
713
+ })
714
+ );
639
715
 
640
716
  message = completion.choices[0].message;
641
717
  }
@@ -655,135 +731,52 @@ You respond with valid JSON only. No markdown, no code blocks, no explanations.`
655
731
  return finalContent;
656
732
  }
657
733
 
658
- async executeAnthropic(prompt, agent = null) {
734
+ async executeAnthropic(prompt, agent = null, memory = []) {
659
735
  if (!process.env.ANTHROPIC_API_KEY) {
660
736
  throw new Error('ANTHROPIC_API_KEY not set in environment');
661
737
  }
662
738
 
663
739
  // Check if agent has teams for delegation
664
740
  const hasTeams = agent && agent.usesTeams && agent.usesTeams.length > 0;
741
+ const mcpToolsDoc = this._getMCPToolsDoc(agent);
665
742
 
666
- const systemPrompt = `You are a Koi agent executor. Your job is to convert user instructions into a precise sequence of executable actions.
743
+ const systemPrompt = `Convert the following instructions into executable JSON actions.
667
744
 
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" }
745
+ OUTPUT: { "actions": [...] }
721
746
 
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
- {
747
+ CRITICAL RULES:
748
+ 1. call_llm: ONLY when playbook says "random", "relacionado", "based on", "adapted", "generate question". If playbook can generate content directly, do NOT use call_llm.
749
+ 2. Loops: "hasta que se despida" while with llm_eval condition
750
+ 3. IDs: Actions MUST have "id" if output will be referenced via \${id.output}
751
+ 4. Template variables ONLY in strings: "text \${var}" not \${var}
752
+ 5. Group consecutive prints with \\n
753
+
754
+ WHILE LOOP EXAMPLE:
755
+ { "id": "name", "intent": "prompt_user", "question": "¿Cuál es tu nombre?" },
756
+ { "intent": "registry_set", "key": "last", "value": "\${name.output.answer}" },
757
+ { "intent": "while",
758
+ "condition": { "llm_eval": true, "instruction": "¿Continuar?", "data": "\${response.output.answer}" },
760
759
  "actions": [
761
- { "actionType": "direct", "intent": "print", "message": "Hello" },
762
- { "actionType": "delegate", "intent": "deleteUser", "data": { "id": "001" } },
763
- { "actionType": "direct", "intent": "print", "message": "User deleted" }
760
+ { "id": "prev", "intent": "registry_get", "key": "last" },
761
+ { "id": "question", "intent": "call_llm", "data": {"answer":"\${prev.output.value}"}, "instruction": "Generate related question" },
762
+ { "id": "response", "intent": "prompt_user", "question": "\${question.output.result}" },
763
+ { "intent": "registry_set", "key": "last", "value": "\${response.output.answer}" }
764
764
  ]
765
765
  }
766
-
767
- CRITICAL: ALWAYS include "actionType" field in EVERY action (either "direct" or "delegate")
766
+ CRITICAL: condition.data uses ID from prompt_user INSIDE loop ("response"), NOT from outside ("name")
768
767
 
769
768
  Available actions:
770
769
  ${actionRegistry.generatePromptDocumentation(agent)}
771
- ${hasTeams && agent ? agent.getPeerCapabilitiesAsActions() : ''}
770
+ ${hasTeams && agent ? await agent.getPeerCapabilitiesAsActions() : ''}
771
+ ${mcpToolsDoc}
772
772
 
773
773
  ${hasTeams ? `\nIMPORTANT: Do NOT nest "intent" inside "data". The "intent" field must be at the top level.` : ''}
774
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
775
+ Data chaining:
776
+ - Reference action outputs: \${actionId.output.field}
777
+ - Template variables ONLY in strings: { "count": "\${user.output.length}" } ✅ NOT { "count": \${user.output.length} } ❌
778
+ - Use descriptive IDs: "user", "question", "response", NOT "a1", "a2", "a3"
779
+ - Examples: \${user.output.name}, \${question.output.result}, \${response.output.answer}
787
780
 
788
781
  CRITICAL: When instructions say "Do NOT add print actions", follow that EXACTLY - only generate the actions listed in the steps.
789
782
  When using "return" actions with data containing template variables, do NOT add intermediate print actions - they will break the data chain.
@@ -796,6 +789,7 @@ REMEMBER: Include print actions for ALL output the user should see, UNLESS the i
796
789
  temperature: 0, // Always use 0 for maximum determinism
797
790
  system: systemPrompt,
798
791
  messages: [
792
+ ...memory,
799
793
  {
800
794
  role: 'user',
801
795
  content: prompt
@@ -816,141 +810,59 @@ REMEMBER: Include print actions for ALL output the user should see, UNLESS the i
816
810
  * @param {Function} onAction - Callback called for each complete action: (action) => void
817
811
  * @returns {Promise<Object>} - Final parsed response
818
812
  */
819
- async executeOpenAIStreaming(prompt, fromDelegation = false, hasTeams = false, promptLength = 0, agent = null, onAction = null) {
813
+ async executeOpenAIStreaming(prompt, fromDelegation = false, hasTeams = false, promptLength = 0, agent = null, onAction = null, memory = []) {
820
814
  if (!process.env.OPENAI_API_KEY) {
821
815
  throw new Error('OPENAI_API_KEY not set in environment');
822
816
  }
823
817
 
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" }
818
+ const mcpToolsDoc = this._getMCPToolsDoc(agent);
880
819
 
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
- }
820
+ // Build system prompt
821
+ const systemPrompt = `Convert the following instructions into executable JSON actions.
908
822
 
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
- }
823
+ OUTPUT: { "actions": [...] }
916
824
 
917
- Example 6 - Without IDs (when results aren't needed):
918
- {
825
+ CRITICAL RULES:
826
+ 1. call_llm: ONLY when playbook says "random", "relacionado", "based on", "adapted", "generate question". If playbook can generate content directly, do NOT use call_llm.
827
+ 2. Loops: "hasta que se despida" → while with llm_eval condition
828
+ 3. IDs: Actions MUST have "id" if output will be referenced via \${id.output}
829
+ 4. Template variables ONLY in strings: "text \${var}" not \${var}
830
+ 5. Group consecutive prints with \\n
831
+
832
+ WHILE LOOP EXAMPLE:
833
+ { "id": "name", "intent": "prompt_user", "question": "¿Cuál es tu nombre?" },
834
+ { "intent": "registry_set", "key": "last", "value": "\${name.output.answer}" },
835
+ { "intent": "while",
836
+ "condition": { "llm_eval": true, "instruction": "¿Continuar?", "data": "\${response.output.answer}" },
919
837
  "actions": [
920
- { "actionType": "direct", "intent": "print", "message": "Hello" },
921
- { "actionType": "delegate", "intent": "deleteUser", "data": { "id": "001" } },
922
- { "actionType": "direct", "intent": "print", "message": "User deleted" }
838
+ { "id": "prev", "intent": "registry_get", "key": "last" },
839
+ { "id": "question", "intent": "call_llm", "data": {"answer":"\${prev.output.value}"}, "instruction": "Generate related question" },
840
+ { "id": "response", "intent": "prompt_user", "question": "\${question.output.result}" },
841
+ { "intent": "registry_set", "key": "last", "value": "\${response.output.answer}" }
923
842
  ]
924
843
  }
925
-
926
- CRITICAL: ALWAYS include "actionType" field in EVERY action (either "direct" or "delegate")
844
+ CRITICAL: condition.data uses ID from prompt_user INSIDE loop ("response"), NOT from outside ("name")
927
845
 
928
846
  Available actions:
929
847
  ${actionRegistry.generatePromptDocumentation(agent)}
930
- ${hasTeams && agent ? agent.getPeerCapabilitiesAsActions() : ''}
848
+ ${hasTeams && agent ? await agent.getPeerCapabilitiesAsActions() : ''}
849
+ ${mcpToolsDoc}
931
850
 
932
851
  ${hasTeams ? `\nIMPORTANT: Do NOT nest "intent" inside "data". The "intent" field must be at the top level.` : ''}
933
852
 
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
853
+ Data chaining:
854
+ - Reference action outputs: \${actionId.output.field}
855
+ - Template variables ONLY in strings: { "count": "\${user.output.length}" } ✅ NOT { "count": \${user.output.length} } ❌
856
+ - Use descriptive IDs: "user", "question", "response", NOT "a1", "a2", "a3"
857
+ - Examples: \${user.output.name}, \${question.output.result}, \${response.output.answer}
946
858
 
947
859
  CRITICAL: When instructions say "Do NOT add print actions", follow that EXACTLY - only generate the actions listed in the steps.
948
860
  When using "return" actions with data containing template variables, do NOT add intermediate print actions - they will break the data chain.
949
861
 
950
862
  REMEMBER: Include print actions for ALL output the user should see, UNLESS the instructions explicitly say not to. Return valid, parseable JSON only.`;
951
863
 
952
- // Use fastest model for delegated work or short playbooks
953
- const model = fromDelegation || promptLength < 500 ? 'gpt-4o-mini' : this.model;
864
+ // Use agent's configured model
865
+ const model = this.model;
954
866
 
955
867
  if (process.env.KOI_DEBUG_LLM) {
956
868
  const agentInfo = agent ? ` | Agent: ${agent.name}` : '';
@@ -965,18 +877,30 @@ REMEMBER: Include print actions for ALL output the user should see, UNLESS the i
965
877
  console.error('─'.repeat(80));
966
878
  }
967
879
 
880
+ // Log memory if present
881
+ if (process.env.KOI_DEBUG_LLM && memory.length > 0) {
882
+ console.error(`[LLM Debug] 🧠 Sending ${memory.length} memory messages:`);
883
+ for (const m of memory) {
884
+ const preview = m.content.substring(0, 150);
885
+ console.error(` [${m.role}] ${preview}...`);
886
+ }
887
+ }
888
+
968
889
  // 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
- });
890
+ const stream = await this.openai.chat.completions.create(
891
+ this.buildApiParams({
892
+ model,
893
+ messages: [
894
+ { role: 'system', content: systemPrompt },
895
+ ...memory,
896
+ { role: 'user', content: prompt }
897
+ ],
898
+ temperature: 0,
899
+ max_tokens: this.maxTokens,
900
+ stream: true, // Enable streaming
901
+ response_format: { type: "json_object" }
902
+ })
903
+ );
980
904
 
981
905
  // Use incremental parser
982
906
  const parser = new IncrementalJSONParser();
@@ -1078,6 +1002,19 @@ REMEMBER: Include print actions for ALL output the user should see, UNLESS the i
1078
1002
  actionQueue.push(...finalActions);
1079
1003
  }
1080
1004
 
1005
+ // Print response immediately after receiving it
1006
+ if (process.env.KOI_DEBUG_LLM) {
1007
+ console.error(`\n[LLM Debug] executeOpenAIStreaming Complete (${fullContent.length} chars)`);
1008
+ console.error('─'.repeat(80));
1009
+ console.error('[LLM Debug] Response:');
1010
+ // Format each line with < prefix and gray color
1011
+ const lines = fullContent.split('\n');
1012
+ for (const line of lines) {
1013
+ console.error(`< \x1b[90m${line}\x1b[0m`);
1014
+ }
1015
+ console.error('─'.repeat(80));
1016
+ }
1017
+
1081
1018
  // Esperar a que se procesen todas las acciones en la cola
1082
1019
  if (processingPromise) {
1083
1020
  await processingPromise;
@@ -1093,18 +1030,6 @@ REMEMBER: Include print actions for ALL output the user should see, UNLESS the i
1093
1030
  throw processingError;
1094
1031
  }
1095
1032
 
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
1033
  return fullContent;
1109
1034
  }
1110
1035
 
@@ -1115,135 +1040,52 @@ REMEMBER: Include print actions for ALL output the user should see, UNLESS the i
1115
1040
  * @param {Function} onAction - Callback called for each complete action: (action) => void
1116
1041
  * @returns {Promise<string>} - Final response content
1117
1042
  */
1118
- async executeAnthropicStreaming(prompt, agent = null, onAction = null) {
1043
+ async executeAnthropicStreaming(prompt, agent = null, onAction = null, memory = []) {
1119
1044
  if (!process.env.ANTHROPIC_API_KEY) {
1120
1045
  throw new Error('ANTHROPIC_API_KEY not set in environment');
1121
1046
  }
1122
1047
 
1123
1048
  // Check if agent has teams for delegation
1124
1049
  const hasTeams = agent && agent.usesTeams && agent.usesTeams.length > 0;
1050
+ const mcpToolsDoc = this._getMCPToolsDoc(agent);
1125
1051
 
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" }
1052
+ const systemPrompt = `Convert the following instructions into executable JSON actions.
1181
1053
 
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" }
1054
+ OUTPUT: { "actions": [...] }
1186
1055
 
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
- {
1056
+ CRITICAL RULES:
1057
+ 1. call_llm: ONLY when playbook says "random", "relacionado", "based on", "adapted", "generate question". If playbook can generate content directly, do NOT use call_llm.
1058
+ 2. Loops: "hasta que se despida" while with llm_eval condition
1059
+ 3. IDs: Actions MUST have "id" if output will be referenced via \${id.output}
1060
+ 4. Template variables ONLY in strings: "text \${var}" not \${var}
1061
+ 5. Group consecutive prints with \\n
1062
+
1063
+ WHILE LOOP EXAMPLE:
1064
+ { "id": "name", "intent": "prompt_user", "question": "¿Cuál es tu nombre?" },
1065
+ { "intent": "registry_set", "key": "last", "value": "\${name.output.answer}" },
1066
+ { "intent": "while",
1067
+ "condition": { "llm_eval": true, "instruction": "¿Continuar?", "data": "\${response.output.answer}" },
1220
1068
  "actions": [
1221
- { "actionType": "direct", "intent": "print", "message": "Hello" },
1222
- { "actionType": "delegate", "intent": "deleteUser", "data": { "id": "001" } },
1223
- { "actionType": "direct", "intent": "print", "message": "User deleted" }
1069
+ { "id": "prev", "intent": "registry_get", "key": "last" },
1070
+ { "id": "question", "intent": "call_llm", "data": {"answer":"\${prev.output.value}"}, "instruction": "Generate related question" },
1071
+ { "id": "response", "intent": "prompt_user", "question": "\${question.output.result}" },
1072
+ { "intent": "registry_set", "key": "last", "value": "\${response.output.answer}" }
1224
1073
  ]
1225
1074
  }
1226
-
1227
- CRITICAL: ALWAYS include "actionType" field in EVERY action (either "direct" or "delegate")
1075
+ CRITICAL: condition.data uses ID from prompt_user INSIDE loop ("response"), NOT from outside ("name")
1228
1076
 
1229
1077
  Available actions:
1230
1078
  ${actionRegistry.generatePromptDocumentation(agent)}
1231
- ${hasTeams && agent ? agent.getPeerCapabilitiesAsActions() : ''}
1079
+ ${hasTeams && agent ? await agent.getPeerCapabilitiesAsActions() : ''}
1080
+ ${mcpToolsDoc}
1232
1081
 
1233
1082
  ${hasTeams ? `\nIMPORTANT: Do NOT nest "intent" inside "data". The "intent" field must be at the top level.` : ''}
1234
1083
 
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
1084
+ Data chaining:
1085
+ - Reference action outputs: \${actionId.output.field}
1086
+ - Template variables ONLY in strings: { "count": "\${user.output.length}" } ✅ NOT { "count": \${user.output.length} } ❌
1087
+ - Use descriptive IDs: "user", "question", "response", NOT "a1", "a2", "a3"
1088
+ - Examples: \${user.output.name}, \${question.output.result}, \${response.output.answer}
1247
1089
 
1248
1090
  CRITICAL: When instructions say "Do NOT add print actions", follow that EXACTLY - only generate the actions listed in the steps.
1249
1091
  When using "return" actions with data containing template variables, do NOT add intermediate print actions - they will break the data chain.
@@ -1269,7 +1111,7 @@ REMEMBER: Include print actions for ALL output the user should see, UNLESS the i
1269
1111
  max_tokens: this.maxTokens,
1270
1112
  temperature: 0,
1271
1113
  system: systemPrompt,
1272
- messages: [{ role: 'user', content: prompt }]
1114
+ messages: [...memory, { role: 'user', content: prompt }]
1273
1115
  });
1274
1116
 
1275
1117
  // Use incremental parser
@@ -1313,14 +1155,10 @@ REMEMBER: Include print actions for ALL output the user should see, UNLESS the i
1313
1155
 
1314
1156
  // Finalize parser to catch any remaining actions
1315
1157
  const finalActions = parser.finalize();
1316
- if (onAction && finalActions.length > 0) {
1317
- for (const action of finalActions) {
1318
- await onAction(action);
1319
- }
1320
- }
1321
1158
 
1159
+ // Print response immediately after receiving it
1322
1160
  if (process.env.KOI_DEBUG_LLM) {
1323
- console.error(`[LLM Debug] executeAnthropicStreaming Complete (${fullContent.length} chars)`);
1161
+ console.error(`\n[LLM Debug] executeAnthropicStreaming Complete (${fullContent.length} chars)`);
1324
1162
  console.error('─'.repeat(80));
1325
1163
  console.error('[LLM Debug] Response:');
1326
1164
  // Format each line with < prefix and gray color
@@ -1331,9 +1169,681 @@ REMEMBER: Include print actions for ALL output the user should see, UNLESS the i
1331
1169
  console.error('─'.repeat(80));
1332
1170
  }
1333
1171
 
1172
+ if (onAction && finalActions.length > 0) {
1173
+ for (const action of finalActions) {
1174
+ await onAction(action);
1175
+ }
1176
+ }
1177
+
1334
1178
  return fullContent;
1335
1179
  }
1336
1180
 
1181
+ // =========================================================================
1182
+ // GEMINI METHODS (uses OpenAI-compatible endpoint)
1183
+ // =========================================================================
1184
+
1185
+ /**
1186
+ * Execute Gemini call (non-streaming, plan-then-execute).
1187
+ * Uses the OpenAI SDK pointed at Gemini's OpenAI-compatible API.
1188
+ */
1189
+ async executeGemini(prompt, agent = null, memory = []) {
1190
+ const hasTeams = agent && agent.usesTeams && agent.usesTeams.length > 0;
1191
+
1192
+ const systemPrompt = `Convert the following instructions into executable JSON actions.
1193
+
1194
+ OUTPUT: { "actions": [...] }
1195
+
1196
+ CRITICAL RULES:
1197
+ 1. call_llm: ONLY when playbook says "random", "relacionado", "based on", "adapted", "generate question". If playbook can generate content directly, do NOT use call_llm.
1198
+ 2. Loops: "hasta que se despida" → while with llm_eval condition
1199
+ 3. IDs: Actions MUST have "id" if output will be referenced via \${id.output}
1200
+ 4. Template variables ONLY in strings: "text \${var}" not \${var}
1201
+ 5. Group consecutive prints with \\n
1202
+
1203
+ Available actions:
1204
+ ${actionRegistry.generatePromptDocumentation(agent)}
1205
+ ${hasTeams && agent ? await agent.getPeerCapabilitiesAsActions() : ''}
1206
+
1207
+ ${hasTeams ? `\nIMPORTANT: Do NOT nest "intent" inside "data". The "intent" field must be at the top level.` : ''}
1208
+
1209
+ Data chaining:
1210
+ - Reference action outputs: \${actionId.output.field}
1211
+ - Template variables ONLY in strings: { "count": "\${user.output.length}" } ✅ NOT { "count": \${user.output.length} } ❌
1212
+ - Use descriptive IDs: "user", "question", "response", NOT "a1", "a2", "a3"
1213
+
1214
+ REMEMBER: Include print actions for ALL output the user should see, UNLESS the instructions explicitly say not to. Return valid, parseable JSON only.`;
1215
+
1216
+ const model = this.model;
1217
+
1218
+ this.logRequest(model, systemPrompt, prompt, `Gemini | Agent: ${agent?.name || 'unknown'}`);
1219
+
1220
+ const completion = await this.openai.chat.completions.create(
1221
+ this.buildApiParams({
1222
+ model,
1223
+ messages: [
1224
+ { role: 'system', content: systemPrompt },
1225
+ ...memory,
1226
+ { role: 'user', content: prompt }
1227
+ ],
1228
+ temperature: 0,
1229
+ max_tokens: this.maxTokens,
1230
+ response_format: { type: 'json_object' }
1231
+ })
1232
+ );
1233
+
1234
+ const content = completion.choices[0].message.content?.trim() || '';
1235
+ this.logResponse(content, `Gemini | Agent: ${agent?.name || 'unknown'}`);
1236
+
1237
+ return content;
1238
+ }
1239
+
1240
+ /**
1241
+ * Execute Gemini call with streaming and incremental action execution.
1242
+ * Uses the OpenAI SDK pointed at Gemini's OpenAI-compatible API.
1243
+ */
1244
+ async executeGeminiStreaming(prompt, hasTeams = false, agent = null, onAction = null, memory = []) {
1245
+ const systemPrompt = `Convert the following instructions into executable JSON actions.
1246
+
1247
+ OUTPUT: { "actions": [...] }
1248
+
1249
+ CRITICAL RULES:
1250
+ 1. call_llm: ONLY when playbook says "random", "relacionado", "based on", "adapted", "generate question". If playbook can generate content directly, do NOT use call_llm.
1251
+ 2. Loops: "hasta que se despida" → while with llm_eval condition
1252
+ 3. IDs: Actions MUST have "id" if output will be referenced via \${id.output}
1253
+ 4. Template variables ONLY in strings: "text \${var}" not \${var}
1254
+ 5. Group consecutive prints with \\n
1255
+
1256
+ Available actions:
1257
+ ${actionRegistry.generatePromptDocumentation(agent)}
1258
+ ${hasTeams && agent ? await agent.getPeerCapabilitiesAsActions() : ''}
1259
+
1260
+ ${hasTeams ? `\nIMPORTANT: Do NOT nest "intent" inside "data". The "intent" field must be at the top level.` : ''}
1261
+
1262
+ Data chaining:
1263
+ - Reference action outputs: \${actionId.output.field}
1264
+ - Template variables ONLY in strings: { "count": "\${user.output.length}" } ✅ NOT { "count": \${user.output.length} } ❌
1265
+ - Use descriptive IDs: "user", "question", "response", NOT "a1", "a2", "a3"
1266
+
1267
+ REMEMBER: Include print actions for ALL output the user should see, UNLESS the instructions explicitly say not to. Return valid, parseable JSON only.`;
1268
+
1269
+ const model = this.model;
1270
+
1271
+ this.logRequest(model, systemPrompt, prompt, `GeminiStreaming | Agent: ${agent?.name || 'unknown'}`);
1272
+
1273
+ // Create streaming completion (same SDK as OpenAI)
1274
+ const stream = await this.openai.chat.completions.create(
1275
+ this.buildApiParams({
1276
+ model,
1277
+ messages: [
1278
+ { role: 'system', content: systemPrompt },
1279
+ ...memory,
1280
+ { role: 'user', content: prompt }
1281
+ ],
1282
+ temperature: 0,
1283
+ max_tokens: this.maxTokens,
1284
+ stream: true,
1285
+ response_format: { type: 'json_object' }
1286
+ })
1287
+ );
1288
+
1289
+ // Use incremental parser (same logic as OpenAI streaming)
1290
+ const parser = new IncrementalJSONParser();
1291
+ let fullContent = '';
1292
+
1293
+ const actionQueue = [];
1294
+ let streamFinished = false;
1295
+ let processingError = null;
1296
+ let isExecuting = false;
1297
+
1298
+ const processQueue = async () => {
1299
+ while (!streamFinished || actionQueue.length > 0) {
1300
+ if (actionQueue.length === 0) {
1301
+ await new Promise(resolve => setTimeout(resolve, 10));
1302
+ continue;
1303
+ }
1304
+ const action = actionQueue.shift();
1305
+ if (!action) continue;
1306
+ try {
1307
+ isExecuting = true;
1308
+ await onAction(action);
1309
+ } catch (error) {
1310
+ processingError = error;
1311
+ break;
1312
+ } finally {
1313
+ isExecuting = false;
1314
+ }
1315
+ }
1316
+ };
1317
+
1318
+ const processingPromise = onAction ? processQueue() : null;
1319
+
1320
+ try {
1321
+ for await (const chunk of stream) {
1322
+ const delta = chunk.choices[0]?.delta?.content || '';
1323
+ if (delta) {
1324
+ fullContent += delta;
1325
+ const actions = parser.feed(delta);
1326
+ if (onAction && actions.length > 0) {
1327
+ actionQueue.push(...actions);
1328
+ }
1329
+ if (processingError) throw processingError;
1330
+ }
1331
+ }
1332
+ streamFinished = true;
1333
+ } catch (error) {
1334
+ streamFinished = true;
1335
+ throw error;
1336
+ }
1337
+
1338
+ const finalActions = parser.finalize();
1339
+ if (onAction && finalActions.length > 0) {
1340
+ actionQueue.push(...finalActions);
1341
+ }
1342
+
1343
+ this.logResponse(fullContent, `GeminiStreaming | Agent: ${agent?.name || 'unknown'}`);
1344
+
1345
+ if (processingPromise) await processingPromise;
1346
+ while (isExecuting) await new Promise(resolve => setTimeout(resolve, 10));
1347
+ if (processingError) throw processingError;
1348
+
1349
+ return fullContent;
1350
+ }
1351
+
1352
+ // =========================================================================
1353
+ // REACTIVE AGENTIC LOOP METHODS
1354
+ // =========================================================================
1355
+
1356
+ /**
1357
+ * Execute one iteration of the reactive playbook loop.
1358
+ * The LLM returns ONE action per call, receives feedback, and adapts.
1359
+ *
1360
+ * @param {Object} params
1361
+ * @param {string} params.playbook - The playbook text
1362
+ * @param {Object} params.context - Context with args and state
1363
+ * @param {string} params.agentName - Agent name for logging
1364
+ * @param {PlaybookSession} params.session - Session tracking state
1365
+ * @param {Object} params.agent - Agent instance
1366
+ * @param {Array} params.conversationHistory - Mutable array of messages
1367
+ * @returns {Object} A single action object
1368
+ */
1369
+ async executePlaybookReactive({ playbook, context, agentName, session, agent, conversationHistory, isFirstCall = false }) {
1370
+ const planningPrefix = agentName ? `[🤖 ${agentName}]` : '';
1371
+ cliLogger.planning(`${planningPrefix} Thinking`);
1372
+
1373
+ if (isFirstCall || conversationHistory.length === 0) {
1374
+ // First call in this handler invocation: build system prompt + send playbook
1375
+ // This fires both when there's no memory (length === 0) AND when memory was
1376
+ // loaded (length > 0) but this is a new handler call (isFirstCall).
1377
+ if (!conversationHistory.some(m => m._system !== undefined)) {
1378
+ const systemPrompt = await this._buildReactiveSystemPrompt(agent);
1379
+ conversationHistory.push({ _system: systemPrompt });
1380
+ }
1381
+
1382
+ const contextStr = Object.keys(context).length > 0
1383
+ ? `\nContext: ${JSON.stringify(context)}`
1384
+ : '';
1385
+
1386
+ // Include MCP connection errors so the LLM can diagnose and inform the user
1387
+ let mcpErrorStr = '';
1388
+ if (session.mcpErrors && Object.keys(session.mcpErrors).length > 0) {
1389
+ const errors = Object.entries(session.mcpErrors)
1390
+ .map(([name, cause]) => `- MCP "${name}" server output:\n${cause}`)
1391
+ .join('\n');
1392
+ mcpErrorStr = `\n\n⚠️ MCP SERVER ERRORS — The following MCP servers crashed on startup. Do NOT call them.\nAnalyze the server output below, identify the root cause, and use "print" to tell the user:\n1. What went wrong (the specific error, not the raw output)\n2. How to fix it (e.g. "run npm install in /path/to/project")\nThen "return" with an error.\n\n${errors}`;
1393
+ }
1394
+
1395
+ conversationHistory.push({
1396
+ role: 'user',
1397
+ content: `PLAYBOOK:\n${playbook}${contextStr}${mcpErrorStr}\n\nReturn your FIRST action.`
1398
+ });
1399
+ } else {
1400
+ // Subsequent iterations: minimal feedback — the LLM has the full
1401
+ // conversation history and can look back at the playbook in message 1.
1402
+ const feedback = session.buildFeedbackContext();
1403
+ conversationHistory.push({
1404
+ role: 'user',
1405
+ content: `${feedback}\nContinue.`
1406
+ });
1407
+ }
1408
+
1409
+ // Call the appropriate provider
1410
+ let responseText;
1411
+ if (this.provider === 'openai') {
1412
+ responseText = await this._callOpenAIReactive(conversationHistory, agent);
1413
+ } else if (this.provider === 'gemini') {
1414
+ responseText = await this._callGeminiReactive(conversationHistory, agent);
1415
+ } else if (this.provider === 'anthropic') {
1416
+ responseText = await this._callAnthropicReactive(conversationHistory, agent);
1417
+ } else {
1418
+ throw new Error(`Unknown provider: ${this.provider}`);
1419
+ }
1420
+
1421
+ cliLogger.clear();
1422
+
1423
+ // Parse the response into a single action
1424
+ const action = this._parseReactiveResponse(responseText);
1425
+
1426
+ // Add assistant message to conversation history
1427
+ conversationHistory.push({
1428
+ role: 'assistant',
1429
+ content: responseText
1430
+ });
1431
+
1432
+ return action;
1433
+ }
1434
+
1435
+ /**
1436
+ * Build system prompt for reactive mode.
1437
+ * Delegates to the unified _buildSystemPrompt.
1438
+ */
1439
+ async _buildReactiveSystemPrompt(agent) {
1440
+ return await this._buildSystemPrompt(agent);
1441
+ }
1442
+
1443
+ /**
1444
+ * Call OpenAI for a reactive loop iteration.
1445
+ * Uses the full conversation history for multi-turn context.
1446
+ */
1447
+ async _callOpenAIReactive(conversationHistory, agent) {
1448
+ if (!process.env.OPENAI_API_KEY) {
1449
+ throw new Error('OPENAI_API_KEY not set in environment');
1450
+ }
1451
+
1452
+ // Build messages array: extract system prompt + filter valid messages
1453
+ const systemPrompt = conversationHistory[0]?._system || '';
1454
+ const messages = [
1455
+ { role: 'system', content: systemPrompt },
1456
+ ...conversationHistory.filter(m => m.role === 'user' || m.role === 'assistant')
1457
+ ];
1458
+
1459
+ const agentInfo = agent ? `Agent: ${agent.name}` : '';
1460
+ this.logRequest(this.model, systemPrompt, messages.filter(m => m.role === 'user').pop()?.content || '', `Reactive ${agentInfo}`);
1461
+
1462
+ const completion = await this.openai.chat.completions.create(
1463
+ this.buildApiParams({
1464
+ model: this.model,
1465
+ messages,
1466
+ temperature: 0,
1467
+ response_format: { type: 'json_object' }
1468
+ })
1469
+ );
1470
+
1471
+ const content = completion.choices[0].message.content?.trim() || '';
1472
+ this.logResponse(content, `Reactive ${agentInfo}`);
1473
+
1474
+ return content;
1475
+ }
1476
+
1477
+ /**
1478
+ * Call Anthropic for a reactive loop iteration.
1479
+ * Extracts system prompt from sentinel and uses multi-turn messages.
1480
+ */
1481
+ async _callAnthropicReactive(conversationHistory, agent) {
1482
+ if (!process.env.ANTHROPIC_API_KEY) {
1483
+ throw new Error('ANTHROPIC_API_KEY not set in environment');
1484
+ }
1485
+
1486
+ // Extract system prompt from sentinel
1487
+ const systemPrompt = conversationHistory[0]?._system || '';
1488
+
1489
+ // Filter to only user/assistant messages
1490
+ const messages = conversationHistory.filter(m => m.role === 'user' || m.role === 'assistant');
1491
+
1492
+ const agentInfo = agent ? `Agent: ${agent.name}` : '';
1493
+ this.logRequest(this.model, systemPrompt, messages.filter(m => m.role === 'user').pop()?.content || '', `Reactive ${agentInfo}`);
1494
+
1495
+ const message = await this.anthropic.messages.create({
1496
+ model: this.model,
1497
+ max_tokens: 8192,
1498
+ temperature: 0,
1499
+ system: systemPrompt,
1500
+ messages
1501
+ });
1502
+
1503
+ const content = message.content[0].text.trim();
1504
+ this.logResponse(content, `Reactive ${agentInfo}`);
1505
+
1506
+ return content;
1507
+ }
1508
+
1509
+ /**
1510
+ * Call Gemini for a reactive loop iteration.
1511
+ * Uses the OpenAI SDK pointed at Gemini's OpenAI-compatible API.
1512
+ */
1513
+ async _callGeminiReactive(conversationHistory, agent) {
1514
+ // Build messages array: extract system prompt + filter valid messages
1515
+ const systemPrompt = conversationHistory[0]?._system || '';
1516
+ const messages = [
1517
+ { role: 'system', content: systemPrompt },
1518
+ ...conversationHistory.filter(m => m.role === 'user' || m.role === 'assistant')
1519
+ ];
1520
+
1521
+ const agentInfo = agent ? `Agent: ${agent.name}` : '';
1522
+ this.logRequest(this.model, systemPrompt, messages.filter(m => m.role === 'user').pop()?.content || '', `GeminiReactive ${agentInfo}`);
1523
+
1524
+ const completion = await this.openai.chat.completions.create(
1525
+ this.buildApiParams({
1526
+ model: this.model,
1527
+ messages,
1528
+ temperature: 0,
1529
+ response_format: { type: 'json_object' }
1530
+ })
1531
+ );
1532
+
1533
+ const content = completion.choices[0].message.content?.trim() || '';
1534
+ this.logResponse(content, `GeminiReactive ${agentInfo}`);
1535
+
1536
+ return content;
1537
+ }
1538
+
1539
+ /**
1540
+ * Parse the LLM response from reactive mode into a single action object.
1541
+ * Handles edge cases like markdown wrapping or legacy array format.
1542
+ */
1543
+ _parseReactiveResponse(responseText) {
1544
+ // Clean markdown code blocks
1545
+ let cleaned = responseText.trim();
1546
+ if (cleaned.startsWith('```')) {
1547
+ cleaned = cleaned.replace(/^```(?:json)?\n?/, '').replace(/\n?```$/, '').trim();
1548
+ }
1549
+
1550
+ let parsed;
1551
+ try {
1552
+ parsed = JSON.parse(cleaned);
1553
+ } catch (e) {
1554
+ throw new Error(`Failed to parse reactive LLM response as JSON: ${e.message}\nResponse: ${cleaned.substring(0, 200)}`);
1555
+ }
1556
+
1557
+ // Handle batched actions: { "batch": [action1, action2, ...] }
1558
+ if (parsed.batch && Array.isArray(parsed.batch) && parsed.batch.length > 0) {
1559
+ this.logDebug(`Reactive response batched ${parsed.batch.length} actions`);
1560
+ const actions = parsed.batch.map(a => this._normalizeReactiveAction(a));
1561
+ return actions.length === 1 ? actions[0] : actions;
1562
+ }
1563
+
1564
+ // Handle raw array (in case json_object mode is not used)
1565
+ if (Array.isArray(parsed)) {
1566
+ if (parsed.length === 0) {
1567
+ throw new Error('Reactive response was an empty array');
1568
+ }
1569
+ const actions = parsed.map(a => this._normalizeReactiveAction(a));
1570
+ return actions.length === 1 ? actions[0] : actions;
1571
+ }
1572
+
1573
+ // If LLM returned legacy format { "actions": [...] }, extract as batch
1574
+ if (parsed.actions && Array.isArray(parsed.actions) && parsed.actions.length > 0) {
1575
+ this.logDebug('Reactive response used legacy {actions:[...]} format, extracting as batch');
1576
+ const actions = parsed.actions.map(a => this._normalizeReactiveAction(a));
1577
+ return actions.length === 1 ? actions[0] : actions;
1578
+ }
1579
+
1580
+ return this._normalizeReactiveAction(parsed);
1581
+ }
1582
+
1583
+ /**
1584
+ * Normalize a single action object from a reactive response.
1585
+ */
1586
+ _normalizeReactiveAction(parsed) {
1587
+ // Safety net: if actionType is not "direct"/"delegate", the LLM put the intent there
1588
+ if (parsed.actionType && parsed.actionType !== 'direct' && parsed.actionType !== 'delegate') {
1589
+ if (!parsed.intent) {
1590
+ parsed.intent = parsed.actionType;
1591
+ }
1592
+ parsed.actionType = 'direct';
1593
+ }
1594
+
1595
+ // Validate minimal structure — if no action fields, treat as raw return data
1596
+ if (!parsed.intent && !parsed.actionType && !parsed.type) {
1597
+ if (Object.keys(parsed).length > 0) {
1598
+ this.logDebug('Reactive response was raw data, wrapping as return action');
1599
+ return { actionType: 'direct', intent: 'return', data: parsed };
1600
+ }
1601
+ throw new Error(`Invalid reactive action: missing "intent" or "actionType". Got: ${JSON.stringify(parsed).substring(0, 200)}`);
1602
+ }
1603
+
1604
+ return parsed;
1605
+ }
1606
+
1607
+ // =========================================================================
1608
+ // UNIFIED SYSTEM PROMPT - shared rules for all execution modes
1609
+ // =========================================================================
1610
+
1611
+ /**
1612
+ * Build the system prompt for all agents.
1613
+ * Single unified prompt — only the available intents change per agent.
1614
+ * @param {Agent} agent - The agent
1615
+ * @returns {string} Complete system prompt
1616
+ */
1617
+ async _buildSystemPrompt(agent) {
1618
+ const hasTeams = agent && agent.usesTeams && agent.usesTeams.length > 0;
1619
+ const resourceSection = await this._buildSmartResourceSection(agent);
1620
+ const intentNesting = hasTeams ? '\nIMPORTANT: Do NOT nest "intent" inside "data". The "intent" field must be at the top level.' : '';
1621
+
1622
+ return `Convert the following instructions into executable JSON actions, STEP BY STEP.
1623
+
1624
+ Return ONE action per response. After each action you receive the result and decide the next.
1625
+ BATCHING: When multiple actions are INDEPENDENT (none depends on a previous one's result), batch them to save round-trips.
1626
+
1627
+ RESPONSE FORMAT:
1628
+ Single: { "actionType": "<type>", "intent": "<action>", ... }
1629
+ Batch: { "batch": [{ action1 }, { action2 }, ...] }
1630
+
1631
+ STRUCTURE:
1632
+ - "actionType": ONLY "direct" or "delegate".
1633
+ - "direct" = built-in action (print, prompt_user, return, registry_set, call_llm, call_mcp, shell, etc.)
1634
+ - "delegate" = send task to a team member
1635
+ - "intent": the specific action to execute
1636
+ - Both fields are ALWAYS required on every action.
1637
+
1638
+ RULES:
1639
+ 1. Return { "actionType": "direct", "intent": "return", "data": {...} } ONLY when ALL instructions have been FULLY completed.
1640
+ 2. If an action fails, TRY A DIFFERENT APPROACH — do NOT repeat the exact same action.
1641
+ 3. For date/age calculations or array iteration, use "call_llm".
1642
+ 4. NEVER give up. If an action fails, find an alternative path.
1643
+ 5. BE PROACTIVE: if an error says how to fix it, use "shell" to fix it, then retry.
1644
+ 6. When instructions complete successfully, return directly — do NOT ask the user.
1645
+ 7. Group consecutive prints with \\n.
1646
+ 8. call_llm: ONLY when instructions say "random", "related to", "based on", "adapted", "generate question". If content can be generated directly, do NOT use call_llm.
1647
+ 9. Use ACTUAL VALUES from conversation history, NOT template variables.
1648
+ 10. If instructions say to repeat N times, you MUST execute ALL N iterations.
1649
+
1650
+ EXAMPLES:
1651
+
1652
+ Delegate: { "actionType": "delegate", "intent": "getUser", "data": { "id": "001" } }
1653
+ Direct: { "actionType": "direct", "intent": "print", "message": "User: Alice" }
1654
+ Return: { "actionType": "direct", "intent": "return", "data": { "success": true } }
1655
+
1656
+ STATE UPDATES: Include "state_updates" in return data to update agent state:
1657
+ { "actionType": "direct", "intent": "return", "data": { "state_updates": { "count": 5 }, "count": 5 } }
1658
+ ${resourceSection}${intentNesting}
1659
+
1660
+ CRITICAL: Return a single JSON action object, or { "batch": [...] } for independent actions. No markdown.`;
1661
+ }
1662
+
1663
+ // =========================================================================
1664
+ // SMART RESOURCE SECTION
1665
+ // =========================================================================
1666
+
1667
+ /**
1668
+ * Build a smart resource section for system prompts.
1669
+ * THE RULE:
1670
+ * - If total intents across ALL resources <= 25: show everything (1-step)
1671
+ * - If total > 25: collapse resources with > 3 intents to summaries (2-step)
1672
+ *
1673
+ * @param {Agent} agent - The agent
1674
+ * @returns {string} Resource documentation for system prompt
1675
+ */
1676
+ async _buildSmartResourceSection(agent) {
1677
+ // 1. Collect ALL resources with their intents
1678
+ const resources = [];
1679
+
1680
+ // Direct actions (from action registry)
1681
+ const directActions = actionRegistry.getAll().filter(a => {
1682
+ if (a.hidden) return false;
1683
+ if (!a.permission) return true;
1684
+ return agent.hasPermission(a.permission);
1685
+ });
1686
+ if (directActions.length > 0) {
1687
+ resources.push({
1688
+ type: 'direct',
1689
+ name: 'Built-in Actions',
1690
+ intents: directActions.map(a => ({
1691
+ name: a.intent || a.type,
1692
+ description: a.description,
1693
+ schema: a.schema,
1694
+ _actionDef: a
1695
+ }))
1696
+ });
1697
+ }
1698
+
1699
+ // Team members (delegation targets)
1700
+ const peerIntents = this._collectPeerIntents(agent);
1701
+ for (const peer of peerIntents) {
1702
+ resources.push({
1703
+ type: 'delegate',
1704
+ name: peer.agentName,
1705
+ intents: peer.handlers.map(h => ({
1706
+ name: h.name,
1707
+ description: h.description,
1708
+ params: h.params
1709
+ }))
1710
+ });
1711
+ }
1712
+
1713
+ // MCP servers
1714
+ const mcpSummaries = agent.getMCPToolsSummary?.() || [];
1715
+ for (const mcp of mcpSummaries) {
1716
+ resources.push({
1717
+ type: 'mcp',
1718
+ name: mcp.name,
1719
+ intents: mcp.tools.map(t => ({
1720
+ name: t.name,
1721
+ description: t.description || t.name,
1722
+ inputSchema: t.inputSchema
1723
+ }))
1724
+ });
1725
+ }
1726
+
1727
+ // 2. Count total intents
1728
+ const totalIntents = resources.reduce((sum, r) => sum + r.intents.length, 0);
1729
+
1730
+ if (process.env.KOI_DEBUG_LLM) {
1731
+ console.error(`[SmartPrompt] Total intents: ${totalIntents} across ${resources.length} resources`);
1732
+ for (const r of resources) {
1733
+ console.error(` [${r.type}] ${r.name}: ${r.intents.length} intents`);
1734
+ }
1735
+ }
1736
+
1737
+ // Always expand all resources (1-step)
1738
+ return this._buildExpandedResourceSection(resources, agent);
1739
+ }
1740
+
1741
+ /**
1742
+ * Collect peer intents (handler names + descriptions) from accessible teams.
1743
+ * @param {Agent} agent
1744
+ * @returns {Array<{agentName, handlers: Array<{name, description}>}>}
1745
+ */
1746
+ _collectPeerIntents(agent) {
1747
+ const result = [];
1748
+ const processedAgents = new Set();
1749
+
1750
+ const collectFrom = (member, teamName) => {
1751
+ if (!member || member === agent || processedAgents.has(member.name)) return;
1752
+ processedAgents.add(member.name);
1753
+
1754
+ if (!member.handlers || Object.keys(member.handlers).length === 0) return;
1755
+
1756
+ const handlers = [];
1757
+ for (const [handlerName, handlerFn] of Object.entries(member.handlers)) {
1758
+ let description = `Handle ${handlerName}`;
1759
+ let params = [];
1760
+
1761
+ // Prefer LLM-generated description from build cache
1762
+ if (handlerFn?.__description__) {
1763
+ description = handlerFn.__description__;
1764
+ } else if (handlerFn?.__playbook__) {
1765
+ // Fallback: first line of playbook
1766
+ const firstLine = handlerFn.__playbook__.split('\n')[0].trim();
1767
+ description = firstLine.replace(/\$\{[^}]+\}/g, '...').substring(0, 80);
1768
+ }
1769
+
1770
+ // Extract required params from ${args.X} patterns in playbook
1771
+ if (handlerFn?.__playbook__) {
1772
+ const paramMatches = handlerFn.__playbook__.matchAll(/\$\{args\.(\w+)/g);
1773
+ params = [...new Set([...paramMatches].map(m => m[1]))];
1774
+ }
1775
+
1776
+ handlers.push({ name: handlerName, description, params });
1777
+ }
1778
+
1779
+ result.push({
1780
+ agentName: teamName ? `${member.name} (${teamName})` : member.name,
1781
+ handlers
1782
+ });
1783
+ };
1784
+
1785
+ // Peers team
1786
+ if (agent.peers?.members) {
1787
+ for (const [name, member] of Object.entries(agent.peers.members)) {
1788
+ collectFrom(member, agent.peers.name);
1789
+ }
1790
+ }
1791
+
1792
+ // Uses teams
1793
+ for (const team of (agent.usesTeams || [])) {
1794
+ if (team?.members) {
1795
+ for (const [name, member] of Object.entries(team.members)) {
1796
+ collectFrom(member, team.name);
1797
+ }
1798
+ }
1799
+ }
1800
+
1801
+ return result;
1802
+ }
1803
+
1804
+ /**
1805
+ * Build expanded resource section - show all intents directly.
1806
+ * This is the normal behavior when total intents <= 25.
1807
+ */
1808
+ _buildExpandedResourceSection(resources, agent) {
1809
+ let doc = '\nAvailable actions:\n';
1810
+
1811
+ // Built-in actions first
1812
+ for (const resource of resources) {
1813
+ if (resource.type === 'direct') {
1814
+ doc += actionRegistry.generatePromptDocumentation(agent);
1815
+ }
1816
+ }
1817
+
1818
+ // Delegate targets
1819
+ for (const resource of resources) {
1820
+ if (resource.type === 'delegate') {
1821
+ doc += `\n${resource.name} (delegate):\n`;
1822
+ for (const handler of resource.intents) {
1823
+ const dataTemplate = handler.params?.length > 0
1824
+ ? `{ ${handler.params.map(p => `"${p}": ...`).join(', ')} }`
1825
+ : '{ ... }';
1826
+ doc += `- { "actionType": "delegate", "intent": "${handler.name}", "data": ${dataTemplate} } - ${handler.description}\n`;
1827
+ }
1828
+ }
1829
+ }
1830
+
1831
+ // MCP servers — each in its own section
1832
+ for (const resource of resources) {
1833
+ if (resource.type === 'mcp') {
1834
+ doc += `\nMCP "${resource.name}" tools:\n`;
1835
+ for (const tool of resource.intents) {
1836
+ const inputDesc = tool.inputSchema?.properties
1837
+ ? Object.keys(tool.inputSchema.properties).map(k => `"${k}": ...`).join(', ')
1838
+ : '';
1839
+ doc += `- { "intent": "call_mcp", "mcp": "${resource.name}", "tool": "${tool.name}", "input": { ${inputDesc} } } - ${tool.description}\n`;
1840
+ }
1841
+ }
1842
+ }
1843
+
1844
+ return doc;
1845
+ }
1846
+
1337
1847
  /**
1338
1848
  * Generate embeddings for semantic search
1339
1849
  * Uses OpenAI's text-embedding-3-small for fast, cheap embeddings
@@ -1344,18 +1854,19 @@ REMEMBER: Include print actions for ALL output the user should see, UNLESS the i
1344
1854
  throw new Error('getEmbedding requires non-empty text input');
1345
1855
  }
1346
1856
 
1347
- if (this.provider === 'openai' || this.provider === 'anthropic') {
1348
- // Always use OpenAI for embeddings (Anthropic doesn't have embeddings API)
1857
+ if (this.provider === 'openai' || this.provider === 'anthropic' || this.provider === 'gemini') {
1858
+ // Always use OpenAI for embeddings (Anthropic/Gemini don't have compatible embeddings API)
1349
1859
  if (!process.env.OPENAI_API_KEY) {
1350
1860
  throw new Error('OPENAI_API_KEY required for embeddings');
1351
1861
  }
1352
1862
 
1353
- if (!this.openai) {
1354
- this.openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
1863
+ // Use a dedicated OpenAI client for embeddings (Gemini's openai client points elsewhere)
1864
+ if (!this._embeddingClient) {
1865
+ this._embeddingClient = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
1355
1866
  }
1356
1867
 
1357
1868
  try {
1358
- const response = await this.openai.embeddings.create({
1869
+ const response = await this._embeddingClient.embeddings.create({
1359
1870
  model: 'text-embedding-3-small',
1360
1871
  input: text.trim()
1361
1872
  });