@koi-language/koi 1.0.6 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +27 -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
package/src/runtime/agent.js
CHANGED
|
@@ -1,20 +1,101 @@
|
|
|
1
1
|
import { LLMProvider } from './llm-provider.js';
|
|
2
2
|
import { cliLogger } from './cli-logger.js';
|
|
3
3
|
import { actionRegistry } from './action-registry.js';
|
|
4
|
+
import { PlaybookSession } from './playbook-session.js';
|
|
5
|
+
import { buildActionDisplay } from './cli-display.js';
|
|
4
6
|
|
|
5
7
|
// Global call stack to detect infinite loops across all agents
|
|
6
8
|
const globalCallStack = [];
|
|
7
9
|
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Use LLM to infer action metadata from playbook
|
|
13
|
+
* @param {string} playbook - The playbook text
|
|
14
|
+
* @returns {Promise<{description: string, inputParams: string, returnType: string}>}
|
|
15
|
+
*/
|
|
16
|
+
async function inferActionMetadata(playbook) {
|
|
17
|
+
try {
|
|
18
|
+
if (process.env.KOI_DEBUG_LLM) {
|
|
19
|
+
console.error('[InferActionMetadata] Analyzing playbook...');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (!process.env.OPENAI_API_KEY) {
|
|
23
|
+
return {
|
|
24
|
+
description: 'Execute action',
|
|
25
|
+
inputParams: '{ ... }',
|
|
26
|
+
returnType: '{ "result": "any" }'
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Call OpenAI API directly
|
|
31
|
+
const fetch = (await import('node-fetch')).default;
|
|
32
|
+
const response = await fetch('https://api.openai.com/v1/chat/completions', {
|
|
33
|
+
method: 'POST',
|
|
34
|
+
headers: {
|
|
35
|
+
'Content-Type': 'application/json',
|
|
36
|
+
'Authorization': `Bearer ${process.env.OPENAI_API_KEY}`
|
|
37
|
+
},
|
|
38
|
+
body: JSON.stringify({
|
|
39
|
+
model: 'gpt-4o-mini',
|
|
40
|
+
temperature: 0,
|
|
41
|
+
messages: [
|
|
42
|
+
{
|
|
43
|
+
role: 'system',
|
|
44
|
+
content: 'Extract action metadata from agent playbooks. Focus on UNIQUE, SPECIFIC characteristics that distinguish this action from others. Return ONLY valid JSON.'
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
role: 'user',
|
|
48
|
+
content: `Analyze this playbook and identify what makes it UNIQUE:\n\n${playbook}\n\nExtract:\n1. description: What makes THIS action unique and specific (15-20 words). Focus on:\n - The specific role/persona (e.g., "left-wing activist", "philosopher", "poet")\n - The unique perspective or style it brings\n - What differentiates it from similar actions\n Example: "Generates radical left-wing political response from activist perspective" NOT "Generates response"\n\n2. inputParams: Input parameters structure (look for \${args.X} references)\n Example: { "context": "string", "conversation": "string" }\n\n3. returnType: Output structure (look for "Return:" or return statements)\n Example: { "answer": "string" }\n\nRespond with JSON:\n{ "description": "...", "inputParams": "{ ... }", "returnType": "{ ... }" }\n\nNO markdown, NO explanations, ONLY JSON.`
|
|
49
|
+
}
|
|
50
|
+
]
|
|
51
|
+
})
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
const data = await response.json();
|
|
55
|
+
let result = data.choices[0].message.content.trim();
|
|
56
|
+
|
|
57
|
+
// Clean up response (remove markdown if present)
|
|
58
|
+
if (result.startsWith('```')) {
|
|
59
|
+
result = result.replace(/```json?\n?/g, '').replace(/```/g, '').trim();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const parsed = JSON.parse(result);
|
|
63
|
+
|
|
64
|
+
if (process.env.KOI_DEBUG_LLM) {
|
|
65
|
+
console.error(`[InferActionMetadata] Result:`, parsed);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
description: parsed.description || 'Execute action',
|
|
70
|
+
inputParams: parsed.inputParams || '{ ... }',
|
|
71
|
+
returnType: parsed.returnType || '{ "result": "any" }'
|
|
72
|
+
};
|
|
73
|
+
} catch (error) {
|
|
74
|
+
if (process.env.KOI_DEBUG_LLM) {
|
|
75
|
+
console.error(`[InferActionMetadata] Error: ${error.message}`);
|
|
76
|
+
}
|
|
77
|
+
// If inference fails, use defaults
|
|
78
|
+
return {
|
|
79
|
+
description: 'Execute action',
|
|
80
|
+
inputParams: '{ ... }',
|
|
81
|
+
returnType: '{ "result": "any" }'
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
8
86
|
export class Agent {
|
|
9
87
|
constructor(config) {
|
|
10
88
|
this.name = config.name;
|
|
11
89
|
this.role = config.role;
|
|
12
90
|
this.skills = config.skills || [];
|
|
13
91
|
this.usesTeams = config.usesTeams || []; // Teams this agent uses as a client
|
|
92
|
+
this.usesMCPNames = config.usesMCP || []; // MCP server names this agent uses
|
|
14
93
|
this.llm = config.llm || { provider: 'openai', model: 'gpt-4', temperature: 0.2 };
|
|
15
94
|
this.state = config.state || {};
|
|
16
95
|
this.playbooks = config.playbooks || {};
|
|
17
96
|
this.resilience = config.resilience || null;
|
|
97
|
+
this.amnesia = config.amnesia || false;
|
|
98
|
+
this.memory = []; // Conversation history across playbook executions
|
|
18
99
|
|
|
19
100
|
// Never allow peers to be null - use a proxy that throws helpful error
|
|
20
101
|
if (config.peers) {
|
|
@@ -191,12 +272,19 @@ export class Agent {
|
|
|
191
272
|
|
|
192
273
|
// Evaluate template string with context (interpolate ${...} expressions)
|
|
193
274
|
// Create a function that evaluates the template in the context of args and state
|
|
275
|
+
// Wraps args/state in Proxy so undefined properties resolve to "" instead of "undefined"
|
|
194
276
|
const evaluateTemplate = (template, context) => {
|
|
195
277
|
try {
|
|
196
|
-
const
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
278
|
+
const safeProxy = (obj) => new Proxy(obj || {}, {
|
|
279
|
+
get(target, prop) {
|
|
280
|
+
const val = target[prop];
|
|
281
|
+
if (val === undefined || val === null) return '';
|
|
282
|
+
if (typeof val === 'object' && !Array.isArray(val)) return safeProxy(val);
|
|
283
|
+
return val;
|
|
284
|
+
}
|
|
285
|
+
});
|
|
286
|
+
const args = safeProxy(context.args);
|
|
287
|
+
const state = safeProxy(context.state);
|
|
200
288
|
const fn = new Function('args', 'state', `return \`${template}\`;`);
|
|
201
289
|
return fn(args, state);
|
|
202
290
|
} catch (error) {
|
|
@@ -219,8 +307,41 @@ export class Agent {
|
|
|
219
307
|
}
|
|
220
308
|
}
|
|
221
309
|
|
|
310
|
+
// Agent memory: pass previous conversation history unless amnesia is enabled
|
|
311
|
+
const memory = this.amnesia ? [] : this.memory;
|
|
312
|
+
if (process.env.KOI_DEBUG_LLM) {
|
|
313
|
+
const memoryTokens = this._estimateTokens(this.memory);
|
|
314
|
+
console.error(`[Agent:${this.name}] 🧠 Memory check: amnesia=${this.amnesia}, messages=${this.memory.length}, ~${memoryTokens} tokens`);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// All agents use reactive loop mode (step by step, one action per LLM call)
|
|
318
|
+
return await this._executePlaybookReactive(eventName, interpolatedPlaybook, args, context, memory);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Plan-then-execute mode: send playbook to LLM in ONE call, get ALL actions back.
|
|
323
|
+
* This is the original execution path, used for delegated tasks.
|
|
324
|
+
*/
|
|
325
|
+
async _executePlaybookPlanThenExecute(eventName, interpolatedPlaybook, args, context, _fromDelegation, selectedTools, memory = []) {
|
|
326
|
+
// Connect MCPs eagerly so their tools appear in the system prompt
|
|
327
|
+
if (this.usesMCPNames.length > 0) {
|
|
328
|
+
const mcpRegistry = globalThis.mcpRegistry;
|
|
329
|
+
if (mcpRegistry) {
|
|
330
|
+
for (const mcpName of this.usesMCPNames) {
|
|
331
|
+
const client = mcpRegistry.get(mcpName);
|
|
332
|
+
if (client && !client.initialized) {
|
|
333
|
+
try {
|
|
334
|
+
await client.connect();
|
|
335
|
+
} catch (err) {
|
|
336
|
+
const cause = client.lastError || err.message;
|
|
337
|
+
console.error(`[Agent:${this.name}] ❌ MCP "${mcpName}" failed to connect: ${cause}`);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
222
344
|
// STREAMING OPTIMIZATION: Execute actions incrementally as they arrive
|
|
223
|
-
// This reduces latency by starting execution before the full JSON is received
|
|
224
345
|
const streamedActions = [];
|
|
225
346
|
const actionContext = {
|
|
226
347
|
state: this.state,
|
|
@@ -232,55 +353,42 @@ export class Agent {
|
|
|
232
353
|
const onStreamAction = async (action) => {
|
|
233
354
|
streamedActions.push(action);
|
|
234
355
|
|
|
235
|
-
// Execute action immediately (sequential execution for proper chaining)
|
|
236
356
|
try {
|
|
237
357
|
const resolvedAction = this.resolveActionReferences(action, actionContext);
|
|
238
358
|
|
|
239
|
-
// Check condition if present
|
|
240
|
-
|
|
359
|
+
// Check condition if present (but skip for actions that handle their own conditions internally)
|
|
360
|
+
const actionsWithOwnConditions = ['if', 'while', 'repeat'];
|
|
361
|
+
if (resolvedAction.condition !== undefined &&
|
|
362
|
+
!actionsWithOwnConditions.includes(resolvedAction.intent) &&
|
|
363
|
+
!actionsWithOwnConditions.includes(resolvedAction.type)) {
|
|
241
364
|
const conditionMet = this.evaluateCondition(resolvedAction.condition, actionContext);
|
|
242
365
|
if (!conditionMet) {
|
|
243
|
-
return;
|
|
366
|
+
return;
|
|
244
367
|
}
|
|
245
368
|
}
|
|
246
369
|
|
|
247
|
-
|
|
248
|
-
const actionTitle = resolvedAction.title || intent;
|
|
249
|
-
cliLogger.progress(`[${this.name}] ${actionTitle}`);
|
|
370
|
+
cliLogger.planning(buildActionDisplay(this.name, resolvedAction));
|
|
250
371
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
// Check if this is a delegation action
|
|
254
|
-
if (action.actionType === 'delegate') {
|
|
255
|
-
// Delegation: route to appropriate team member
|
|
256
|
-
if (process.env.KOI_DEBUG_LLM) {
|
|
257
|
-
console.error(`[Agent:${this.name}] 🔀 Delegating action: ${action.intent}`);
|
|
258
|
-
}
|
|
259
|
-
result = await this.resolveAction(resolvedAction, actionContext);
|
|
260
|
-
} else {
|
|
261
|
-
// Direct action: check if this is a registered action with an executor
|
|
262
|
-
const actionDef = actionRegistry.get(action.intent || action.type);
|
|
263
|
-
|
|
264
|
-
if (actionDef && actionDef.execute) {
|
|
265
|
-
// Fast path: execute registered action
|
|
266
|
-
result = await actionDef.execute(resolvedAction, this);
|
|
267
|
-
} else if (action.intent || action.description) {
|
|
268
|
-
// Resolve via router (legacy fallback)
|
|
269
|
-
result = await this.resolveAction(resolvedAction, actionContext);
|
|
270
|
-
} else {
|
|
271
|
-
// Fallback legacy
|
|
272
|
-
result = await this.executeLegacyAction(resolvedAction);
|
|
273
|
-
}
|
|
274
|
-
}
|
|
372
|
+
const { result } = await this._executeAction(action, resolvedAction, actionContext);
|
|
275
373
|
|
|
276
374
|
cliLogger.clear();
|
|
277
375
|
|
|
278
|
-
// Update context for next action (chaining)
|
|
279
376
|
if (result && typeof result === 'object') {
|
|
280
|
-
|
|
377
|
+
let unwrappedResult = result;
|
|
378
|
+
if (result.result && typeof result.result === 'string' && Object.keys(result).length === 1) {
|
|
379
|
+
try {
|
|
380
|
+
const parsed = JSON.parse(result.result);
|
|
381
|
+
if (typeof parsed === 'object') {
|
|
382
|
+
unwrappedResult = parsed;
|
|
383
|
+
}
|
|
384
|
+
} catch (e) {
|
|
385
|
+
// Not JSON, keep as-is
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
const resultForContext = JSON.parse(JSON.stringify(unwrappedResult));
|
|
281
390
|
actionContext.results.push(resultForContext);
|
|
282
391
|
|
|
283
|
-
// Store result with action ID for explicit referencing
|
|
284
392
|
if (action.id) {
|
|
285
393
|
actionContext[action.id] = { output: resultForContext };
|
|
286
394
|
|
|
@@ -290,11 +398,8 @@ export class Agent {
|
|
|
290
398
|
}
|
|
291
399
|
}
|
|
292
400
|
|
|
293
|
-
// Only update previousResult for actions that produce meaningful data
|
|
294
|
-
// Side-effect actions like print, log (that return metadata) should not override previousResult
|
|
295
401
|
const nonDataActions = ['print', 'log', 'format'];
|
|
296
|
-
|
|
297
|
-
if (!nonDataActions.includes(intent)) {
|
|
402
|
+
if (!nonDataActions.includes(action.intent || action.type)) {
|
|
298
403
|
actionContext.previousResult = resultForContext;
|
|
299
404
|
actionContext.lastResult = resultForContext;
|
|
300
405
|
}
|
|
@@ -312,7 +417,6 @@ export class Agent {
|
|
|
312
417
|
}
|
|
313
418
|
};
|
|
314
419
|
|
|
315
|
-
// Execute playbook with streaming (onAction callback receives each action as it completes)
|
|
316
420
|
const result = await this.llmProvider.executePlaybook(
|
|
317
421
|
interpolatedPlaybook,
|
|
318
422
|
context,
|
|
@@ -320,16 +424,37 @@ export class Agent {
|
|
|
320
424
|
selectedTools,
|
|
321
425
|
this,
|
|
322
426
|
_fromDelegation,
|
|
323
|
-
onStreamAction
|
|
427
|
+
onStreamAction,
|
|
428
|
+
memory
|
|
324
429
|
);
|
|
325
430
|
|
|
326
|
-
// If streaming was used, actions were already executed
|
|
327
431
|
if (streamedActions.length > 0) {
|
|
328
|
-
|
|
329
|
-
const finalResult = actionContext.results.length > 0
|
|
432
|
+
let finalResult = actionContext.results.length > 0
|
|
330
433
|
? actionContext.results[actionContext.results.length - 1]
|
|
331
434
|
: {};
|
|
332
435
|
|
|
436
|
+
// Apply state updates if returned by LLM (streaming path)
|
|
437
|
+
if (finalResult && typeof finalResult === 'object') {
|
|
438
|
+
if (finalResult.state_updates || finalResult.stateUpdates) {
|
|
439
|
+
const updates = finalResult.state_updates || finalResult.stateUpdates;
|
|
440
|
+
Object.keys(updates).forEach(key => {
|
|
441
|
+
this.state[key] = updates[key];
|
|
442
|
+
});
|
|
443
|
+
// Strip state_updates from returned result
|
|
444
|
+
const { state_updates, stateUpdates, ...resultData } = finalResult;
|
|
445
|
+
finalResult = resultData;
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// Save conversation to agent memory (unless amnesia is enabled)
|
|
450
|
+
// Save the ACTUAL executed result, not the raw LLM action plan
|
|
451
|
+
if (!this.amnesia) {
|
|
452
|
+
this._saveToMemory([
|
|
453
|
+
{ role: 'user', content: interpolatedPlaybook },
|
|
454
|
+
{ role: 'assistant', content: JSON.stringify(finalResult) }
|
|
455
|
+
]);
|
|
456
|
+
}
|
|
457
|
+
|
|
333
458
|
if (actionContext.results.length > 1) {
|
|
334
459
|
return {
|
|
335
460
|
...finalResult,
|
|
@@ -341,13 +466,18 @@ export class Agent {
|
|
|
341
466
|
return finalResult;
|
|
342
467
|
}
|
|
343
468
|
|
|
469
|
+
// Save conversation to agent memory for non-streaming path
|
|
470
|
+
if (!this.amnesia) {
|
|
471
|
+
this._saveToMemory([
|
|
472
|
+
{ role: 'user', content: interpolatedPlaybook },
|
|
473
|
+
{ role: 'assistant', content: JSON.stringify(result) }
|
|
474
|
+
]);
|
|
475
|
+
}
|
|
476
|
+
|
|
344
477
|
// No streaming - handle traditional execution path
|
|
345
|
-
// Handle malformed responses - if LLM didn't return actions or result, try to extract
|
|
346
478
|
if (result && !result.actions && !result.result) {
|
|
347
|
-
// LLM returned unexpected format - try to find actions in other fields
|
|
348
479
|
for (const key of Object.keys(result)) {
|
|
349
480
|
if (Array.isArray(result[key]) && result[key].length > 0 && result[key][0].type) {
|
|
350
|
-
// Found array of actions under different key
|
|
351
481
|
console.warn(`[${this.name}] ⚠️ LLM returned actions under "${key}" instead of "actions" - fixing`);
|
|
352
482
|
result.actions = result[key];
|
|
353
483
|
delete result[key];
|
|
@@ -356,29 +486,14 @@ export class Agent {
|
|
|
356
486
|
}
|
|
357
487
|
}
|
|
358
488
|
|
|
359
|
-
// Check if LLM returned actions (new action-based system)
|
|
360
489
|
if (result && result.actions && Array.isArray(result.actions)) {
|
|
361
|
-
// Decision: Should this agent execute actions or return them?
|
|
362
|
-
//
|
|
363
|
-
// Execute actions if:
|
|
364
|
-
// - NOT called from delegation (orchestrators always execute)
|
|
365
|
-
// - OR agent is a specialized worker (has no teams to delegate to)
|
|
366
|
-
// Workers should execute their specialized actions (registry ops, tool calls, etc.)
|
|
367
|
-
// even when called from delegation
|
|
368
490
|
const canDelegateToTeams = this.usesTeams && this.usesTeams.length > 0;
|
|
369
491
|
const shouldExecuteActions = !_fromDelegation || !canDelegateToTeams;
|
|
370
492
|
|
|
371
493
|
if (shouldExecuteActions) {
|
|
372
|
-
// Don't log action count - not useful information
|
|
373
|
-
// console.log(`[${this.name}] → ${result.actions.length} actions`);
|
|
374
|
-
|
|
375
|
-
// Extract any additional fields the LLM provided (plan, explanation, etc.)
|
|
376
494
|
const { actions, ...additionalFields } = result;
|
|
377
|
-
|
|
378
|
-
// Execute the actions
|
|
379
495
|
const actionResults = await this.executeActions(actions);
|
|
380
496
|
|
|
381
|
-
// If there are additional fields, merge them with the action results
|
|
382
497
|
if (Object.keys(additionalFields).length > 0) {
|
|
383
498
|
return {
|
|
384
499
|
...additionalFields,
|
|
@@ -388,14 +503,9 @@ export class Agent {
|
|
|
388
503
|
|
|
389
504
|
return actionResults;
|
|
390
505
|
} else {
|
|
391
|
-
// Agent is an orchestrator called from delegation - don't execute nested delegation
|
|
392
|
-
// (This prevents infinite loops where orchestrators try to delegate from within delegation)
|
|
393
506
|
console.log(`[${this.name}] ⚠️ Ignoring nested actions (orchestrator in delegated call)`);
|
|
394
|
-
|
|
395
|
-
// Return the result without the actions field
|
|
396
507
|
const { actions, ...actualResult } = result;
|
|
397
508
|
|
|
398
|
-
// If there's no other data besides actions, try to extract from first action
|
|
399
509
|
if (Object.keys(actualResult).length === 0 && actions.length > 0) {
|
|
400
510
|
const firstAction = actions[0];
|
|
401
511
|
return firstAction.data || firstAction.result || {};
|
|
@@ -407,17 +517,13 @@ export class Agent {
|
|
|
407
517
|
|
|
408
518
|
// Legacy support: Apply state updates if returned by LLM
|
|
409
519
|
if (result && typeof result === 'object') {
|
|
410
|
-
// Check if LLM returned state updates
|
|
411
520
|
if (result.state_updates || result.stateUpdates) {
|
|
412
521
|
const updates = result.state_updates || result.stateUpdates;
|
|
413
|
-
|
|
414
|
-
// Apply updates to agent state
|
|
415
522
|
Object.keys(updates).forEach(key => {
|
|
416
523
|
this.state[key] = updates[key];
|
|
417
524
|
});
|
|
418
525
|
}
|
|
419
526
|
|
|
420
|
-
// If result has both state_updates and other fields, return just the result fields
|
|
421
527
|
if (result.state_updates || result.stateUpdates) {
|
|
422
528
|
const { state_updates, stateUpdates, ...resultData } = result;
|
|
423
529
|
return resultData;
|
|
@@ -427,6 +533,259 @@ export class Agent {
|
|
|
427
533
|
return result;
|
|
428
534
|
}
|
|
429
535
|
|
|
536
|
+
/**
|
|
537
|
+
* Reactive agentic loop: LLM decides ONE action per iteration,
|
|
538
|
+
* receives feedback, and adapts its strategy.
|
|
539
|
+
* Used for top-level orchestration (not delegated tasks).
|
|
540
|
+
*/
|
|
541
|
+
async _executePlaybookReactive(eventName, interpolatedPlaybook, args, context, memory = []) {
|
|
542
|
+
const session = new PlaybookSession({
|
|
543
|
+
playbook: interpolatedPlaybook,
|
|
544
|
+
agentName: this.name
|
|
545
|
+
});
|
|
546
|
+
session.actionContext.args = args;
|
|
547
|
+
session.actionContext.state = this.state;
|
|
548
|
+
|
|
549
|
+
// Seed conversation history with previous memory
|
|
550
|
+
const conversationHistory = [];
|
|
551
|
+
if (memory.length > 0) {
|
|
552
|
+
// Build system prompt first (needed as sentinel for provider extraction)
|
|
553
|
+
const systemPrompt = await this.llmProvider._buildReactiveSystemPrompt(this);
|
|
554
|
+
conversationHistory.push({ _system: systemPrompt });
|
|
555
|
+
// Insert previous conversation turns as context
|
|
556
|
+
conversationHistory.push(...memory);
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
// Connect MCPs eagerly so their tools appear in the system prompt
|
|
560
|
+
const mcpErrors = {};
|
|
561
|
+
if (this.usesMCPNames.length > 0) {
|
|
562
|
+
const mcpRegistry = globalThis.mcpRegistry;
|
|
563
|
+
if (mcpRegistry) {
|
|
564
|
+
for (const mcpName of this.usesMCPNames) {
|
|
565
|
+
const client = mcpRegistry.get(mcpName);
|
|
566
|
+
if (client && !client.initialized) {
|
|
567
|
+
try {
|
|
568
|
+
await client.connect();
|
|
569
|
+
} catch (err) {
|
|
570
|
+
const cause = client.lastError || err.message;
|
|
571
|
+
mcpErrors[mcpName] = cause;
|
|
572
|
+
console.error(`[Agent:${this.name}] ❌ MCP "${mcpName}" failed to connect: ${cause}`);
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
// If any MCPs failed, inject the errors so the LLM knows
|
|
580
|
+
if (Object.keys(mcpErrors).length > 0) {
|
|
581
|
+
session.mcpErrors = mcpErrors;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
if (process.env.KOI_DEBUG_LLM) {
|
|
585
|
+
console.error(`[Agent:${this.name}] 🔄 Starting reactive loop`);
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
let isFirstCall = true;
|
|
589
|
+
while (session.canContinue()) {
|
|
590
|
+
// 1. GET ACTION(S) from LLM — may be a single action or a batched array
|
|
591
|
+
let response;
|
|
592
|
+
try {
|
|
593
|
+
response = await this.llmProvider.executePlaybookReactive({
|
|
594
|
+
playbook: interpolatedPlaybook,
|
|
595
|
+
context,
|
|
596
|
+
agentName: this.name,
|
|
597
|
+
session,
|
|
598
|
+
agent: this,
|
|
599
|
+
conversationHistory,
|
|
600
|
+
isFirstCall
|
|
601
|
+
});
|
|
602
|
+
isFirstCall = false;
|
|
603
|
+
} catch (error) {
|
|
604
|
+
if (process.env.KOI_DEBUG_LLM) {
|
|
605
|
+
console.error(`[Agent:${this.name}] ❌ LLM call failed: ${error.message}`);
|
|
606
|
+
}
|
|
607
|
+
session.recordAction({ intent: '_llm_error', actionType: 'direct' }, null, error);
|
|
608
|
+
continue;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
// Normalize to array for uniform processing
|
|
612
|
+
const actionBatch = Array.isArray(response) ? response : [response];
|
|
613
|
+
|
|
614
|
+
if (process.env.KOI_DEBUG_LLM && actionBatch.length > 1) {
|
|
615
|
+
console.error(`[Agent:${this.name}] 📦 Batched ${actionBatch.length} actions`);
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
// Process each action in the batch sequentially
|
|
619
|
+
let terminated = false;
|
|
620
|
+
for (const action of actionBatch) {
|
|
621
|
+
if (!session.canContinue()) break;
|
|
622
|
+
|
|
623
|
+
if (process.env.KOI_DEBUG_LLM) {
|
|
624
|
+
const intent = action.intent || action.type || 'unknown';
|
|
625
|
+
console.error(`[Agent:${this.name}] 🎯 Reactive step ${session.iteration + 1}: ${intent}`);
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
// CHECK TERMINAL ACTION
|
|
629
|
+
if ((action.intent || action.type) === 'return') {
|
|
630
|
+
const resolved = this.resolveActionReferences(action, session.actionContext);
|
|
631
|
+
let returnData = resolved.data || {};
|
|
632
|
+
|
|
633
|
+
// Apply state updates if present
|
|
634
|
+
if (returnData && typeof returnData === 'object' && (returnData.state_updates || returnData.stateUpdates)) {
|
|
635
|
+
const updates = returnData.state_updates || returnData.stateUpdates;
|
|
636
|
+
Object.keys(updates).forEach(key => {
|
|
637
|
+
this.state[key] = updates[key];
|
|
638
|
+
});
|
|
639
|
+
const { state_updates, stateUpdates, ...cleanData } = returnData;
|
|
640
|
+
returnData = cleanData;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
session.terminate(returnData);
|
|
644
|
+
terminated = true;
|
|
645
|
+
break;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
// EXECUTE ACTION (reusing _executeAction)
|
|
649
|
+
try {
|
|
650
|
+
const resolved = this.resolveActionReferences(action, session.actionContext);
|
|
651
|
+
|
|
652
|
+
// Check condition (skip for actions that handle their own conditions)
|
|
653
|
+
const actionsWithOwnConditions = ['if', 'while', 'repeat'];
|
|
654
|
+
if (resolved.condition !== undefined &&
|
|
655
|
+
!actionsWithOwnConditions.includes(resolved.intent) &&
|
|
656
|
+
!actionsWithOwnConditions.includes(resolved.type)) {
|
|
657
|
+
if (!this.evaluateCondition(resolved.condition, session.actionContext)) {
|
|
658
|
+
session.recordAction(action, { skipped: true });
|
|
659
|
+
continue;
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
cliLogger.planning(buildActionDisplay(this.name, resolved));
|
|
664
|
+
|
|
665
|
+
const { result } = await this._executeAction(action, resolved, session.actionContext);
|
|
666
|
+
|
|
667
|
+
cliLogger.clear();
|
|
668
|
+
session.recordAction(action, result);
|
|
669
|
+
|
|
670
|
+
if (process.env.KOI_DEBUG_LLM) {
|
|
671
|
+
const preview = result ? JSON.stringify(result).substring(0, 150) : 'null';
|
|
672
|
+
console.error(`[Agent:${this.name}] ✅ Result: ${preview}`);
|
|
673
|
+
}
|
|
674
|
+
} catch (error) {
|
|
675
|
+
cliLogger.clear();
|
|
676
|
+
if (process.env.KOI_DEBUG_LLM) {
|
|
677
|
+
console.error(`[Agent:${this.name}] ❌ Action failed: ${error.message}`);
|
|
678
|
+
}
|
|
679
|
+
session.recordAction(action, null, error);
|
|
680
|
+
// On error in a batch, stop processing remaining actions so LLM can react
|
|
681
|
+
break;
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
if (terminated) break;
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
if (process.env.KOI_DEBUG_LLM) {
|
|
689
|
+
console.error(`[Agent:${this.name}] 🔄 Reactive loop finished after ${session.iteration} iterations`);
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
// Save conversation to agent memory (unless amnesia is enabled)
|
|
693
|
+
if (!this.amnesia) {
|
|
694
|
+
// Extract only user/assistant messages (skip system sentinel)
|
|
695
|
+
const newMessages = conversationHistory.filter(m => m.role === 'user' || m.role === 'assistant');
|
|
696
|
+
// Only save the new messages (skip the ones that came from previous memory)
|
|
697
|
+
const newCount = newMessages.length - memory.length;
|
|
698
|
+
if (newCount > 0) {
|
|
699
|
+
this._saveToMemory(newMessages.slice(-newCount));
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
// Return final result
|
|
704
|
+
if (session.finalResult) return session.finalResult;
|
|
705
|
+
|
|
706
|
+
// Fallback: return last result if loop exhausted
|
|
707
|
+
const results = session.actionContext.results;
|
|
708
|
+
return results.length > 0 ? results[results.length - 1] : {};
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
/**
|
|
712
|
+
* Save messages to agent memory, trimming to max size.
|
|
713
|
+
* @private
|
|
714
|
+
*/
|
|
715
|
+
_saveToMemory(messages) {
|
|
716
|
+
const maxMemory = this.llm.maxMemory || 50;
|
|
717
|
+
this.memory.push(...messages);
|
|
718
|
+
if (this.memory.length > maxMemory) {
|
|
719
|
+
// Trim oldest messages, keeping the last maxMemory entries
|
|
720
|
+
this.memory = this.memory.slice(-maxMemory);
|
|
721
|
+
}
|
|
722
|
+
if (process.env.KOI_DEBUG_LLM) {
|
|
723
|
+
const memoryTokens = this._estimateTokens(this.memory);
|
|
724
|
+
console.error(`[Agent:${this.name}] 🧠 Memory saved (${this.memory.length} messages, ~${memoryTokens} tokens)`);
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
/**
|
|
729
|
+
* Estimate token count for an array of messages (~4 chars per token).
|
|
730
|
+
* @private
|
|
731
|
+
*/
|
|
732
|
+
_estimateTokens(messages) {
|
|
733
|
+
let chars = 0;
|
|
734
|
+
for (const msg of messages) {
|
|
735
|
+
if (typeof msg.content === 'string') {
|
|
736
|
+
chars += msg.content.length;
|
|
737
|
+
} else if (msg.content) {
|
|
738
|
+
chars += JSON.stringify(msg.content).length;
|
|
739
|
+
}
|
|
740
|
+
if (msg.role) chars += msg.role.length;
|
|
741
|
+
}
|
|
742
|
+
return Math.ceil(chars / 4);
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
/**
|
|
746
|
+
* Execute a single action (common code for both streaming and batch execution)
|
|
747
|
+
* @private
|
|
748
|
+
* @returns {Object} { result, shouldExitLoop }
|
|
749
|
+
*/
|
|
750
|
+
async _executeAction(action, resolvedAction, context) {
|
|
751
|
+
const actionRegistry = (await import('./action-registry.js')).actionRegistry;
|
|
752
|
+
let result;
|
|
753
|
+
let shouldExitLoop = false;
|
|
754
|
+
|
|
755
|
+
// Check if this is a delegation action
|
|
756
|
+
if (action.actionType === 'delegate') {
|
|
757
|
+
// Delegation: route to appropriate team member
|
|
758
|
+
if (process.env.KOI_DEBUG_LLM) {
|
|
759
|
+
console.error(`[Agent:${this.name}] 🔀 Delegating action: ${action.intent}`);
|
|
760
|
+
}
|
|
761
|
+
result = await this.resolveAction(resolvedAction, context);
|
|
762
|
+
} else {
|
|
763
|
+
// Direct action: check if this is a registered action with an executor
|
|
764
|
+
const actionDef = actionRegistry.get(action.intent || action.type);
|
|
765
|
+
|
|
766
|
+
if (actionDef && actionDef.execute) {
|
|
767
|
+
// Fast path: execute registered action
|
|
768
|
+
// Store current context in agent for actions that need it (like 'if', 'while', etc.)
|
|
769
|
+
this._currentActionContext = context;
|
|
770
|
+
result = await actionDef.execute(resolvedAction, this);
|
|
771
|
+
delete this._currentActionContext;
|
|
772
|
+
|
|
773
|
+
// Special handling for return action with conditions
|
|
774
|
+
if ((action.intent === 'return' || action.type === 'return') && action.condition !== undefined) {
|
|
775
|
+
shouldExitLoop = true;
|
|
776
|
+
}
|
|
777
|
+
} else if (action.intent || action.description) {
|
|
778
|
+
// Resolve via router (legacy fallback)
|
|
779
|
+
result = await this.resolveAction(resolvedAction, context);
|
|
780
|
+
} else {
|
|
781
|
+
// Fallback legacy
|
|
782
|
+
result = await this.executeLegacyAction(resolvedAction);
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
return { result, shouldExitLoop };
|
|
787
|
+
}
|
|
788
|
+
|
|
430
789
|
async executeActions(actions) {
|
|
431
790
|
let finalResult = {};
|
|
432
791
|
let context = {
|
|
@@ -442,7 +801,8 @@ export class Agent {
|
|
|
442
801
|
|
|
443
802
|
// Check if action has a condition - skip if condition is false
|
|
444
803
|
// IMPORTANT: Evaluate condition BEFORE the action executes, using current context
|
|
445
|
-
if
|
|
804
|
+
// EXCEPTION: Skip this check for "if" action which handles its own condition internally
|
|
805
|
+
if (resolvedAction.condition !== undefined && resolvedAction.intent !== 'if' && resolvedAction.type !== 'if') {
|
|
446
806
|
const conditionMet = this.evaluateCondition(resolvedAction.condition, context);
|
|
447
807
|
if (!conditionMet) {
|
|
448
808
|
// Skip this action silently
|
|
@@ -455,37 +815,18 @@ export class Agent {
|
|
|
455
815
|
// Show what action is being executed
|
|
456
816
|
// Use the "title" field if the LLM provided one, otherwise use intent
|
|
457
817
|
const actionTitle = resolvedAction.title || intent;
|
|
458
|
-
cliLogger.progress(`[${this.name}]
|
|
459
|
-
|
|
460
|
-
//
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
if (actionDef && actionDef.execute) {
|
|
472
|
-
// Fast path: execute registered action
|
|
473
|
-
finalResult = await actionDef.execute(resolvedAction, this);
|
|
474
|
-
|
|
475
|
-
// Special handling for return action with conditions
|
|
476
|
-
if ((action.intent === 'return' || action.type === 'return') && action.condition !== undefined) {
|
|
477
|
-
context.results.push(finalResult);
|
|
478
|
-
context.previousResult = finalResult;
|
|
479
|
-
context.lastResult = finalResult;
|
|
480
|
-
i = actions.length; // Exit loop
|
|
481
|
-
}
|
|
482
|
-
} else if (action.intent || action.description) {
|
|
483
|
-
// Resolve via router (legacy fallback)
|
|
484
|
-
finalResult = await this.resolveAction(resolvedAction, context);
|
|
485
|
-
} else {
|
|
486
|
-
// Fallback legacy
|
|
487
|
-
finalResult = await this.executeLegacyAction(resolvedAction);
|
|
488
|
-
}
|
|
818
|
+
cliLogger.progress(`[${this.name}] Thinking...`);
|
|
819
|
+
|
|
820
|
+
// Execute the action using common method
|
|
821
|
+
const { result, shouldExitLoop } = await this._executeAction(action, resolvedAction, context);
|
|
822
|
+
finalResult = result;
|
|
823
|
+
|
|
824
|
+
// Handle special return action with conditions
|
|
825
|
+
if (shouldExitLoop) {
|
|
826
|
+
context.results.push(finalResult);
|
|
827
|
+
context.previousResult = finalResult;
|
|
828
|
+
context.lastResult = finalResult;
|
|
829
|
+
i = actions.length; // Exit loop
|
|
489
830
|
}
|
|
490
831
|
|
|
491
832
|
// Clear progress after action completes
|
|
@@ -572,10 +913,6 @@ export class Agent {
|
|
|
572
913
|
// Deep clone to avoid mutating original
|
|
573
914
|
const resolved = JSON.parse(JSON.stringify(action));
|
|
574
915
|
|
|
575
|
-
if (process.env.KOI_DEBUG_LLM) {
|
|
576
|
-
console.error(`[Agent:${this.name}] 🔄 Resolving references for action: ${action.intent || action.type}`);
|
|
577
|
-
}
|
|
578
|
-
|
|
579
916
|
// DON'T resolve condition here - it will be evaluated directly in evaluateCondition()
|
|
580
917
|
// (Conditions need special handling to preserve boolean expressions)
|
|
581
918
|
|
|
@@ -611,6 +948,23 @@ export class Agent {
|
|
|
611
948
|
resolved.text = this.resolveObjectReferences(resolved.text, context);
|
|
612
949
|
}
|
|
613
950
|
|
|
951
|
+
// Resolve references in question field (for prompt_user action)
|
|
952
|
+
if (resolved.question !== undefined) {
|
|
953
|
+
resolved.question = this.resolveObjectReferences(resolved.question, context);
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
// Resolve references in mcp/tool fields (for call_mcp action)
|
|
957
|
+
if (resolved.mcp !== undefined) {
|
|
958
|
+
resolved.mcp = this.resolveObjectReferences(resolved.mcp, context);
|
|
959
|
+
}
|
|
960
|
+
if (resolved.tool !== undefined) {
|
|
961
|
+
resolved.tool = this.resolveObjectReferences(resolved.tool, context);
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
if (resolved.description !== undefined) {
|
|
965
|
+
resolved.description = this.resolveObjectReferences(resolved.description, context);
|
|
966
|
+
}
|
|
967
|
+
|
|
614
968
|
return resolved;
|
|
615
969
|
}
|
|
616
970
|
|
|
@@ -628,9 +982,10 @@ export class Agent {
|
|
|
628
982
|
JSON, // Allow JSON methods
|
|
629
983
|
Math // Allow Math methods
|
|
630
984
|
};
|
|
631
|
-
// Add all action IDs from context (
|
|
985
|
+
// Add all action IDs from context (any key with .output)
|
|
986
|
+
const reservedKeys = ['previousResult', 'lastResult', 'results', 'state', 'args'];
|
|
632
987
|
for (const key in context) {
|
|
633
|
-
if (
|
|
988
|
+
if (!reservedKeys.includes(key) && context[key]?.output !== undefined) {
|
|
634
989
|
evalContext[key] = context[key];
|
|
635
990
|
}
|
|
636
991
|
}
|
|
@@ -773,8 +1128,15 @@ export class Agent {
|
|
|
773
1128
|
const evalContext = { ...this.buildEvalContext(context), ...context };
|
|
774
1129
|
const fn = new Function(...Object.keys(evalContext), `return ${expr};`);
|
|
775
1130
|
const result = fn(...Object.values(evalContext));
|
|
776
|
-
// Convert to
|
|
777
|
-
|
|
1131
|
+
// Convert to proper JavaScript literal for condition evaluation
|
|
1132
|
+
if (typeof result === 'string') {
|
|
1133
|
+
// Quote strings properly to avoid "undefined variable" errors
|
|
1134
|
+
return `'${result.replace(/'/g, "\\'")}'`;
|
|
1135
|
+
} else if (typeof result === 'object') {
|
|
1136
|
+
return JSON.stringify(result);
|
|
1137
|
+
} else {
|
|
1138
|
+
return String(result);
|
|
1139
|
+
}
|
|
778
1140
|
} catch (error) {
|
|
779
1141
|
console.warn(`[Agent:${this.name}] Failed to evaluate condition sub-expression "${expr}": ${error.message}`);
|
|
780
1142
|
return match;
|
|
@@ -835,7 +1197,7 @@ export class Agent {
|
|
|
835
1197
|
// 2️⃣ Do I have a matching skill?
|
|
836
1198
|
const matchingSkill = this.findMatchingSkill(intent);
|
|
837
1199
|
if (matchingSkill) {
|
|
838
|
-
cliLogger.
|
|
1200
|
+
cliLogger.planning(buildActionDisplay(this.name, action));
|
|
839
1201
|
const result = await this.callSkill(matchingSkill, action.data || action.input || {});
|
|
840
1202
|
cliLogger.clear();
|
|
841
1203
|
globalCallStack.pop();
|
|
@@ -844,6 +1206,12 @@ export class Agent {
|
|
|
844
1206
|
|
|
845
1207
|
// 3️⃣ Can someone in my teams handle it? (check peers + usesTeams)
|
|
846
1208
|
if (this.peers || this.usesTeams.length > 0) {
|
|
1209
|
+
// Check delegate permission
|
|
1210
|
+
if (!this.hasPermission('delegate')) {
|
|
1211
|
+
globalCallStack.pop();
|
|
1212
|
+
throw new Error(`Agent "${this.name}" cannot delegate: role "${this.role?.name || 'unknown'}" lacks "can delegate" permission.`);
|
|
1213
|
+
}
|
|
1214
|
+
|
|
847
1215
|
// Search within team members - team defines communication boundaries
|
|
848
1216
|
const teamMember = await this.findTeamMemberForIntent(intent);
|
|
849
1217
|
|
|
@@ -913,20 +1281,13 @@ export class Agent {
|
|
|
913
1281
|
return null;
|
|
914
1282
|
}
|
|
915
1283
|
|
|
916
|
-
const { agentRouter } = await import('./router.js');
|
|
917
|
-
|
|
918
|
-
// Get all potential matches from the global router
|
|
919
|
-
let matches = await agentRouter.findMatches(intent, 10);
|
|
920
|
-
|
|
921
1284
|
// Collect all teams this agent can access
|
|
922
1285
|
const accessibleTeams = [];
|
|
923
1286
|
|
|
924
|
-
// Add peers team (if this agent is a member of a team)
|
|
925
1287
|
if (this.peers && this.peers.members) {
|
|
926
1288
|
accessibleTeams.push(this.peers);
|
|
927
1289
|
}
|
|
928
1290
|
|
|
929
|
-
// Add usesTeams (teams this agent uses as a client)
|
|
930
1291
|
for (const team of this.usesTeams) {
|
|
931
1292
|
if (team && team.members) {
|
|
932
1293
|
accessibleTeams.push(team);
|
|
@@ -937,18 +1298,34 @@ export class Agent {
|
|
|
937
1298
|
return null;
|
|
938
1299
|
}
|
|
939
1300
|
|
|
940
|
-
//
|
|
1301
|
+
// 1. Try direct handler name matching first (no LLM call needed!)
|
|
1302
|
+
for (const team of accessibleTeams) {
|
|
1303
|
+
for (const memberName of Object.keys(team.members)) {
|
|
1304
|
+
const member = team.members[memberName];
|
|
1305
|
+
if (member === this) continue;
|
|
1306
|
+
|
|
1307
|
+
const matchingEvent = member.findMatchingHandler(intent);
|
|
1308
|
+
if (matchingEvent) {
|
|
1309
|
+
if (process.env.KOI_DEBUG_LLM) {
|
|
1310
|
+
console.error(`[Agent:${this.name}] ✅ Direct match: ${member.name}.${matchingEvent} for intent "${intent}"`);
|
|
1311
|
+
}
|
|
1312
|
+
return { agent: member, event: matchingEvent };
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
// 2. No direct match — use semantic router as fallback
|
|
1318
|
+
const { agentRouter } = await import('./router.js');
|
|
1319
|
+
let matches = await agentRouter.findMatches(intent, 10);
|
|
1320
|
+
|
|
1321
|
+
// Filter to only include agents in accessible teams (exclude self)
|
|
941
1322
|
matches = matches.filter(match => {
|
|
942
|
-
// Check if this agent is in any accessible team
|
|
943
1323
|
const isAccessible = accessibleTeams.some(team => {
|
|
944
|
-
|
|
945
|
-
return teamMemberNames.some(name => {
|
|
1324
|
+
return Object.keys(team.members).some(name => {
|
|
946
1325
|
const member = team.members[name];
|
|
947
1326
|
return member === match.agent || member.name === match.agent.name;
|
|
948
1327
|
});
|
|
949
1328
|
});
|
|
950
|
-
|
|
951
|
-
// Also exclude self
|
|
952
1329
|
return isAccessible && match.agent !== this;
|
|
953
1330
|
});
|
|
954
1331
|
|
|
@@ -956,20 +1333,6 @@ export class Agent {
|
|
|
956
1333
|
return matches[0];
|
|
957
1334
|
}
|
|
958
1335
|
|
|
959
|
-
// Fallback: Try direct handler matching in accessible team members
|
|
960
|
-
for (const team of accessibleTeams) {
|
|
961
|
-
const memberNames = Object.keys(team.members);
|
|
962
|
-
for (const memberName of memberNames) {
|
|
963
|
-
const member = team.members[memberName];
|
|
964
|
-
if (member === this) continue; // Skip self
|
|
965
|
-
|
|
966
|
-
const matchingEvent = member.findMatchingHandler(intent);
|
|
967
|
-
if (matchingEvent) {
|
|
968
|
-
return { agent: member, event: matchingEvent };
|
|
969
|
-
}
|
|
970
|
-
}
|
|
971
|
-
}
|
|
972
|
-
|
|
973
1336
|
return null;
|
|
974
1337
|
}
|
|
975
1338
|
|
|
@@ -1115,12 +1478,12 @@ export class Agent {
|
|
|
1115
1478
|
* Generate peer capabilities formatted as available actions
|
|
1116
1479
|
* Returns a string listing delegation actions in the same format as action registry
|
|
1117
1480
|
*/
|
|
1118
|
-
getPeerCapabilitiesAsActions() {
|
|
1481
|
+
async getPeerCapabilitiesAsActions() {
|
|
1119
1482
|
const capabilities = [];
|
|
1120
1483
|
const processedAgents = new Set();
|
|
1121
1484
|
|
|
1122
1485
|
// Helper function to collect handlers from an agent
|
|
1123
|
-
const collectHandlers = (agent, teamName = null) => {
|
|
1486
|
+
const collectHandlers = async (agent, teamName = null) => {
|
|
1124
1487
|
if (!agent || processedAgents.has(agent.name)) {
|
|
1125
1488
|
return;
|
|
1126
1489
|
}
|
|
@@ -1133,46 +1496,40 @@ export class Agent {
|
|
|
1133
1496
|
|
|
1134
1497
|
// Extract affordance/description from handler
|
|
1135
1498
|
let description = '';
|
|
1499
|
+
let inputParams = '{ ... }';
|
|
1500
|
+
let returnType = null;
|
|
1136
1501
|
const handlerFn = agent.handlers[handler];
|
|
1137
1502
|
|
|
1138
1503
|
if (handlerFn && handlerFn.__playbook__) {
|
|
1139
|
-
// Extract first line or first sentence from playbook as description
|
|
1140
1504
|
const playbook = handlerFn.__playbook__;
|
|
1141
|
-
const lines = playbook.split('\n');
|
|
1142
|
-
const firstLine = lines[0].trim();
|
|
1143
1505
|
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
if (description.length < firstLine.length) {
|
|
1147
|
-
description += '...';
|
|
1506
|
+
if (process.env.KOI_DEBUG_LLM) {
|
|
1507
|
+
console.error(`[CollectHandlers] Found playbook for ${handler}, length: ${playbook.length}`);
|
|
1148
1508
|
}
|
|
1149
1509
|
|
|
1150
|
-
//
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
const fields = returnContent.match(/"([^"]+)":/g);
|
|
1159
|
-
if (fields) {
|
|
1160
|
-
const fieldNames = fields.map(f => f.replace(/[":]/g, '')).join(', ');
|
|
1161
|
-
description += ` → Returns: {${fieldNames}}`;
|
|
1162
|
-
}
|
|
1163
|
-
break;
|
|
1164
|
-
}
|
|
1510
|
+
// Use LLM to infer metadata from playbook
|
|
1511
|
+
const metadata = await inferActionMetadata(playbook);
|
|
1512
|
+
description = metadata.description;
|
|
1513
|
+
inputParams = metadata.inputParams;
|
|
1514
|
+
returnType = metadata.returnType;
|
|
1515
|
+
|
|
1516
|
+
if (process.env.KOI_DEBUG_LLM) {
|
|
1517
|
+
console.error(`[CollectHandlers] Inferred metadata for ${handler}:`, metadata);
|
|
1165
1518
|
}
|
|
1166
1519
|
} else if (handlerFn && typeof handlerFn === 'function') {
|
|
1167
1520
|
// For regular functions, generate description from name
|
|
1168
1521
|
description = `Handle ${handler} event`;
|
|
1522
|
+
inputParams = '{ ... }';
|
|
1523
|
+
returnType = '{ "result": "any" }';
|
|
1169
1524
|
}
|
|
1170
1525
|
|
|
1171
1526
|
capabilities.push({
|
|
1172
1527
|
intent: handler,
|
|
1173
1528
|
agent: agentInfo,
|
|
1174
1529
|
role: agent.role ? agent.role.name : 'Unknown',
|
|
1175
|
-
description: description || `Execute ${handler}
|
|
1530
|
+
description: description || `Execute ${handler}`,
|
|
1531
|
+
inputParams: inputParams,
|
|
1532
|
+
returnType: returnType || '{ "result": "any" }'
|
|
1176
1533
|
});
|
|
1177
1534
|
}
|
|
1178
1535
|
}
|
|
@@ -1184,7 +1541,7 @@ export class Agent {
|
|
|
1184
1541
|
for (const memberName of memberNames) {
|
|
1185
1542
|
const member = this.peers.members[memberName];
|
|
1186
1543
|
if (member !== this) {
|
|
1187
|
-
collectHandlers(member, this.peers.name);
|
|
1544
|
+
await collectHandlers(member, this.peers.name);
|
|
1188
1545
|
}
|
|
1189
1546
|
}
|
|
1190
1547
|
}
|
|
@@ -1195,7 +1552,7 @@ export class Agent {
|
|
|
1195
1552
|
const memberNames = Object.keys(team.members);
|
|
1196
1553
|
for (const memberName of memberNames) {
|
|
1197
1554
|
const member = team.members[memberName];
|
|
1198
|
-
collectHandlers(member, team.name);
|
|
1555
|
+
await collectHandlers(member, team.name);
|
|
1199
1556
|
}
|
|
1200
1557
|
}
|
|
1201
1558
|
}
|
|
@@ -1206,7 +1563,8 @@ export class Agent {
|
|
|
1206
1563
|
|
|
1207
1564
|
let doc = '\n\nDelegation actions (to team members):\n';
|
|
1208
1565
|
for (const cap of capabilities) {
|
|
1209
|
-
|
|
1566
|
+
// Build delegation description with inferred metadata
|
|
1567
|
+
doc += `- { "actionType": "delegate", "intent": "${cap.intent}", "data": ${cap.inputParams} } - ${cap.description} → Returns: ${cap.returnType} (Delegate to ${cap.agent} [${cap.role}])\n`;
|
|
1210
1568
|
}
|
|
1211
1569
|
|
|
1212
1570
|
return doc;
|
|
@@ -1324,17 +1682,48 @@ Execute this task and return the result as JSON.
|
|
|
1324
1682
|
}
|
|
1325
1683
|
|
|
1326
1684
|
|
|
1327
|
-
async callSkill(skillName,
|
|
1328
|
-
// Calling skill
|
|
1329
|
-
|
|
1685
|
+
async callSkill(skillName, functionNameOrInput, inputOrUndefined) {
|
|
1330
1686
|
if (!this.skills.includes(skillName)) {
|
|
1331
1687
|
throw new Error(`Agent ${this.name} does not have skill: ${skillName}`);
|
|
1332
1688
|
}
|
|
1333
1689
|
|
|
1334
|
-
//
|
|
1335
|
-
//
|
|
1336
|
-
//
|
|
1337
|
-
|
|
1690
|
+
// Support two calling conventions:
|
|
1691
|
+
// 1. callSkill(skillName, functionName, input) - call specific function
|
|
1692
|
+
// 2. callSkill(skillName, input) - legacy: find matching function by intent
|
|
1693
|
+
let functionName, input;
|
|
1694
|
+
|
|
1695
|
+
if (inputOrUndefined !== undefined) {
|
|
1696
|
+
// Convention 1: explicit function name
|
|
1697
|
+
functionName = functionNameOrInput;
|
|
1698
|
+
input = inputOrUndefined;
|
|
1699
|
+
} else {
|
|
1700
|
+
// Convention 2: auto-select function (legacy)
|
|
1701
|
+
input = functionNameOrInput;
|
|
1702
|
+
|
|
1703
|
+
// Try to find a matching function using skill selector
|
|
1704
|
+
// For now, we'll just use the first available function
|
|
1705
|
+
const skillFunctions = globalThis.SkillRegistry?.getAll(skillName);
|
|
1706
|
+
if (!skillFunctions || Object.keys(skillFunctions).length === 0) {
|
|
1707
|
+
throw new Error(`No functions found in skill: ${skillName}`);
|
|
1708
|
+
}
|
|
1709
|
+
|
|
1710
|
+
functionName = Object.keys(skillFunctions)[0];
|
|
1711
|
+
}
|
|
1712
|
+
|
|
1713
|
+
// Get the function from SkillRegistry
|
|
1714
|
+
const skillFunction = globalThis.SkillRegistry?.get(skillName, functionName);
|
|
1715
|
+
|
|
1716
|
+
if (!skillFunction) {
|
|
1717
|
+
throw new Error(`Function ${functionName} not found in skill ${skillName}`);
|
|
1718
|
+
}
|
|
1719
|
+
|
|
1720
|
+
// Execute the skill function
|
|
1721
|
+
try {
|
|
1722
|
+
const result = await skillFunction.fn(input);
|
|
1723
|
+
return result;
|
|
1724
|
+
} catch (error) {
|
|
1725
|
+
throw new Error(`Skill ${skillName}.${functionName} failed: ${error.message}`);
|
|
1726
|
+
}
|
|
1338
1727
|
}
|
|
1339
1728
|
|
|
1340
1729
|
/**
|
|
@@ -1362,6 +1751,31 @@ Execute this task and return the result as JSON.
|
|
|
1362
1751
|
}
|
|
1363
1752
|
|
|
1364
1753
|
|
|
1754
|
+
/**
|
|
1755
|
+
* Get MCP tool summaries for system prompt generation.
|
|
1756
|
+
* Returns tool info from all MCP servers this agent has access to.
|
|
1757
|
+
*/
|
|
1758
|
+
getMCPToolsSummary() {
|
|
1759
|
+
const mcpRegistry = globalThis.mcpRegistry;
|
|
1760
|
+
if (!mcpRegistry || this.usesMCPNames.length === 0) return [];
|
|
1761
|
+
|
|
1762
|
+
const summaries = [];
|
|
1763
|
+
for (const mcpName of this.usesMCPNames) {
|
|
1764
|
+
const client = mcpRegistry.get(mcpName);
|
|
1765
|
+
if (client && client.tools.length > 0) {
|
|
1766
|
+
summaries.push({
|
|
1767
|
+
name: mcpName,
|
|
1768
|
+
tools: client.tools.map(t => ({
|
|
1769
|
+
name: t.name,
|
|
1770
|
+
description: t.description || '',
|
|
1771
|
+
inputSchema: t.inputSchema
|
|
1772
|
+
}))
|
|
1773
|
+
});
|
|
1774
|
+
}
|
|
1775
|
+
}
|
|
1776
|
+
return summaries;
|
|
1777
|
+
}
|
|
1778
|
+
|
|
1365
1779
|
toString() {
|
|
1366
1780
|
return `Agent(${this.name}:${this.role.name})`;
|
|
1367
1781
|
}
|