@koi-language/koi 1.0.5 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -125
- package/examples/.build/agent-dialogue.ts +138 -0
- package/examples/.build/agent-dialogue.ts.map +1 -0
- package/examples/.build/chess.ts +77 -0
- package/examples/.build/chess.ts.map +1 -0
- package/examples/.build/delegation-test.ts +140 -0
- package/examples/.build/delegation-test.ts.map +1 -0
- package/examples/.build/dialog-demo.ts +77 -0
- package/examples/.build/dialog-demo.ts.map +1 -0
- package/examples/.build/hello-world.ts +77 -0
- package/examples/.build/hello-world.ts.map +1 -0
- package/examples/.build/lover-dialog-demo.ts +77 -0
- package/examples/.build/lover-dialog-demo.ts.map +1 -0
- package/examples/.build/package.json +3 -0
- package/examples/.build/registry-interactive-demo.ts +202 -0
- package/examples/.build/registry-interactive-demo.ts.map +1 -0
- package/examples/.build/registry-playbook-demo.ts +201 -0
- package/examples/.build/registry-playbook-demo.ts.map +1 -0
- package/examples/.build/tic-tac-toe.ts +77 -0
- package/examples/.build/tic-tac-toe.ts.map +1 -0
- package/examples/actions-demo.koi +8 -9
- package/examples/activists-dialogue.koi +75 -0
- package/examples/agent-dialogue.koi +66 -0
- package/examples/chess.koi +19 -0
- package/examples/counter.koi +20 -69
- package/examples/delegation-test.koi +16 -18
- package/examples/dialog-demo.koi +20 -0
- package/examples/hello-world.koi +7 -43
- package/examples/mcp-stdio-demo.koi +29 -0
- package/examples/memory-test.koi +49 -0
- package/examples/mobile-mcp-demo.koi +32 -0
- package/examples/multi-event-handler-test.koi +16 -18
- package/examples/pipeline.koi +15 -17
- package/examples/prompt-demo.koi +20 -0
- package/examples/{registry-playbook-email-compositor.koi → registry-interactive-demo.koi} +27 -27
- package/examples/registry-playbook-demo.koi +28 -28
- package/examples/skill-import-test.koi +7 -9
- package/examples/skills/.build/math-operations.ts +1656 -0
- package/examples/skills/.build/math-operations.ts.map +1 -0
- package/examples/skills/.build/package.json +3 -0
- package/examples/skills/.build/string-operations.ts +1643 -0
- package/examples/skills/.build/string-operations.ts.map +1 -0
- package/examples/skills/advanced/.build/index.ts +3223 -0
- package/examples/skills/advanced/.build/index.ts.map +1 -0
- package/examples/skills/advanced/.build/package.json +3 -0
- package/examples/skills/advanced/index.koi +3 -5
- package/examples/skills/math-operations.koi +1 -3
- package/examples/skills/string-operations.koi +1 -3
- package/examples/tic-tac-toe.koi +19 -0
- package/examples/utils/echo-mcp-server.js +141 -0
- package/examples/web-delegation-demo.koi +15 -17
- package/package.json +2 -1
- package/src/cli/koi.js +30 -41
- package/src/compiler/build-optimizer.js +204 -289
- package/src/compiler/cache-manager.js +1 -1
- package/src/compiler/import-resolver.js +5 -9
- package/src/compiler/parser.js +6072 -3476
- package/src/compiler/transpiler.js +346 -38
- package/src/grammar/koi.pegjs +302 -62
- package/src/runtime/actions/{format.js → call-llm.js} +37 -44
- package/src/runtime/actions/call-mcp.js +97 -0
- package/src/runtime/actions/if.js +179 -0
- package/src/runtime/actions/print.js +3 -1
- package/src/runtime/actions/prompt-user.js +75 -0
- package/src/runtime/actions/repeat.js +147 -0
- package/src/runtime/actions/shell.js +185 -0
- package/src/runtime/actions/while.js +205 -0
- package/src/runtime/agent.js +592 -178
- package/src/runtime/cli-display.js +26 -0
- package/src/runtime/cli-input.js +421 -0
- package/src/runtime/cli-logger.js +2 -5
- package/src/runtime/cli-markdown.js +61 -0
- package/src/runtime/cli-select.js +106 -0
- package/src/runtime/incremental-json-parser.js +51 -17
- package/src/runtime/index.js +1 -0
- package/src/runtime/llm-provider.js +1083 -572
- package/src/runtime/mcp-registry.js +141 -0
- package/src/runtime/mcp-stdio-client.js +334 -0
- package/src/runtime/planner.js +1 -1
- package/src/runtime/playbook-session.js +259 -0
- package/src/runtime/registry-backends/keyv-sqlite.js +1 -1
- package/src/runtime/registry-backends/local.js +1 -1
- package/src/runtime/router.js +22 -26
- package/src/runtime/runtime.js +7 -1
- package/examples/cache-test.koi +0 -29
- package/examples/calculator.koi +0 -61
- package/examples/clear-registry.js +0 -33
- package/examples/clear-registry.koi +0 -30
- package/examples/code-introspection-test.koi +0 -149
- package/examples/directory-import-test.koi +0 -84
- package/examples/hello-world-claude.koi +0 -52
- package/examples/hello.koi +0 -24
- package/examples/mcp-example.koi +0 -70
- package/examples/new-import-test.koi +0 -89
- package/examples/registry-demo.koi +0 -184
- package/examples/registry-playbook-email-compositor-2.koi +0 -140
- package/examples/sentiment.koi +0 -90
- package/examples/simple.koi +0 -48
- package/examples/task-chaining-demo.koi +0 -244
- package/examples/test-await.koi +0 -22
- package/examples/test-crypto-sha256.koi +0 -196
- package/examples/test-delegation.koi +0 -41
- package/examples/test-multi-team-routing.koi +0 -258
- package/examples/test-no-handler.koi +0 -35
- package/examples/test-npm-import.koi +0 -67
- package/examples/test-parse.koi +0 -10
- package/examples/test-peers-with-team.koi +0 -59
- package/examples/test-permissions-fail.koi +0 -20
- package/examples/test-permissions.koi +0 -36
- package/examples/test-simple-registry.koi +0 -31
- package/examples/test-typescript-import.koi +0 -64
- package/examples/test-uses-team-syntax.koi +0 -25
- package/examples/test-uses-team.koi +0 -31
|
@@ -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
|
|
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-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
{ "
|
|
347
|
-
{ "
|
|
348
|
-
{ "
|
|
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
|
|
361
|
-
-
|
|
362
|
-
- Template variables
|
|
363
|
-
-
|
|
364
|
-
-
|
|
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
|
|
380
|
-
const 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
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
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
|
|
553
|
-
const 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
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
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
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
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 = `
|
|
743
|
+
const systemPrompt = `Convert the following instructions into executable JSON actions.
|
|
667
744
|
|
|
668
|
-
|
|
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
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
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
|
-
{ "
|
|
762
|
-
{ "
|
|
763
|
-
{ "
|
|
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
|
|
776
|
-
-
|
|
777
|
-
- Template variables
|
|
778
|
-
-
|
|
779
|
-
-
|
|
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
|
-
|
|
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
|
-
|
|
882
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
{ "
|
|
921
|
-
{ "
|
|
922
|
-
{ "
|
|
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
|
|
935
|
-
-
|
|
936
|
-
- Template variables
|
|
937
|
-
-
|
|
938
|
-
-
|
|
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
|
|
953
|
-
const 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
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
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 = `
|
|
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
|
-
|
|
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
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
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
|
-
{ "
|
|
1222
|
-
{ "
|
|
1223
|
-
{ "
|
|
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
|
|
1236
|
-
-
|
|
1237
|
-
- Template variables
|
|
1238
|
-
-
|
|
1239
|
-
-
|
|
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(
|
|
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
|
|
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
|
-
|
|
1354
|
-
|
|
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.
|
|
1869
|
+
const response = await this._embeddingClient.embeddings.create({
|
|
1359
1870
|
model: 'text-embedding-3-small',
|
|
1360
1871
|
input: text.trim()
|
|
1361
1872
|
});
|