@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.
Files changed (113) hide show
  1. package/README.md +4 -125
  2. package/examples/.build/agent-dialogue.ts +138 -0
  3. package/examples/.build/agent-dialogue.ts.map +1 -0
  4. package/examples/.build/chess.ts +77 -0
  5. package/examples/.build/chess.ts.map +1 -0
  6. package/examples/.build/delegation-test.ts +140 -0
  7. package/examples/.build/delegation-test.ts.map +1 -0
  8. package/examples/.build/dialog-demo.ts +77 -0
  9. package/examples/.build/dialog-demo.ts.map +1 -0
  10. package/examples/.build/hello-world.ts +77 -0
  11. package/examples/.build/hello-world.ts.map +1 -0
  12. package/examples/.build/lover-dialog-demo.ts +77 -0
  13. package/examples/.build/lover-dialog-demo.ts.map +1 -0
  14. package/examples/.build/package.json +3 -0
  15. package/examples/.build/registry-interactive-demo.ts +202 -0
  16. package/examples/.build/registry-interactive-demo.ts.map +1 -0
  17. package/examples/.build/registry-playbook-demo.ts +201 -0
  18. package/examples/.build/registry-playbook-demo.ts.map +1 -0
  19. package/examples/.build/tic-tac-toe.ts +77 -0
  20. package/examples/.build/tic-tac-toe.ts.map +1 -0
  21. package/examples/actions-demo.koi +8 -9
  22. package/examples/activists-dialogue.koi +75 -0
  23. package/examples/agent-dialogue.koi +66 -0
  24. package/examples/chess.koi +19 -0
  25. package/examples/counter.koi +20 -69
  26. package/examples/delegation-test.koi +16 -18
  27. package/examples/dialog-demo.koi +20 -0
  28. package/examples/hello-world.koi +7 -43
  29. package/examples/mcp-stdio-demo.koi +29 -0
  30. package/examples/memory-test.koi +49 -0
  31. package/examples/mobile-mcp-demo.koi +32 -0
  32. package/examples/multi-event-handler-test.koi +16 -18
  33. package/examples/pipeline.koi +15 -17
  34. package/examples/prompt-demo.koi +20 -0
  35. package/examples/{registry-playbook-email-compositor.koi → registry-interactive-demo.koi} +27 -27
  36. package/examples/registry-playbook-demo.koi +28 -28
  37. package/examples/skill-import-test.koi +7 -9
  38. package/examples/skills/.build/math-operations.ts +1656 -0
  39. package/examples/skills/.build/math-operations.ts.map +1 -0
  40. package/examples/skills/.build/package.json +3 -0
  41. package/examples/skills/.build/string-operations.ts +1643 -0
  42. package/examples/skills/.build/string-operations.ts.map +1 -0
  43. package/examples/skills/advanced/.build/index.ts +3223 -0
  44. package/examples/skills/advanced/.build/index.ts.map +1 -0
  45. package/examples/skills/advanced/.build/package.json +3 -0
  46. package/examples/skills/advanced/index.koi +3 -5
  47. package/examples/skills/math-operations.koi +1 -3
  48. package/examples/skills/string-operations.koi +1 -3
  49. package/examples/tic-tac-toe.koi +19 -0
  50. package/examples/utils/echo-mcp-server.js +141 -0
  51. package/examples/web-delegation-demo.koi +15 -17
  52. package/package.json +2 -1
  53. package/src/cli/koi.js +30 -41
  54. package/src/compiler/build-optimizer.js +204 -289
  55. package/src/compiler/cache-manager.js +1 -1
  56. package/src/compiler/import-resolver.js +5 -9
  57. package/src/compiler/parser.js +6072 -3476
  58. package/src/compiler/transpiler.js +346 -38
  59. package/src/grammar/koi.pegjs +302 -62
  60. package/src/runtime/actions/{format.js → call-llm.js} +37 -44
  61. package/src/runtime/actions/call-mcp.js +97 -0
  62. package/src/runtime/actions/if.js +179 -0
  63. package/src/runtime/actions/print.js +3 -1
  64. package/src/runtime/actions/prompt-user.js +75 -0
  65. package/src/runtime/actions/repeat.js +147 -0
  66. package/src/runtime/actions/shell.js +185 -0
  67. package/src/runtime/actions/while.js +205 -0
  68. package/src/runtime/agent.js +592 -178
  69. package/src/runtime/cli-display.js +26 -0
  70. package/src/runtime/cli-input.js +421 -0
  71. package/src/runtime/cli-logger.js +2 -5
  72. package/src/runtime/cli-markdown.js +61 -0
  73. package/src/runtime/cli-select.js +106 -0
  74. package/src/runtime/incremental-json-parser.js +51 -17
  75. package/src/runtime/index.js +1 -0
  76. package/src/runtime/llm-provider.js +1083 -572
  77. package/src/runtime/mcp-registry.js +141 -0
  78. package/src/runtime/mcp-stdio-client.js +334 -0
  79. package/src/runtime/planner.js +1 -1
  80. package/src/runtime/playbook-session.js +259 -0
  81. package/src/runtime/registry-backends/keyv-sqlite.js +1 -1
  82. package/src/runtime/registry-backends/local.js +1 -1
  83. package/src/runtime/router.js +22 -26
  84. package/src/runtime/runtime.js +7 -1
  85. package/examples/cache-test.koi +0 -29
  86. package/examples/calculator.koi +0 -61
  87. package/examples/clear-registry.js +0 -33
  88. package/examples/clear-registry.koi +0 -30
  89. package/examples/code-introspection-test.koi +0 -149
  90. package/examples/directory-import-test.koi +0 -84
  91. package/examples/hello-world-claude.koi +0 -52
  92. package/examples/hello.koi +0 -24
  93. package/examples/mcp-example.koi +0 -70
  94. package/examples/new-import-test.koi +0 -89
  95. package/examples/registry-demo.koi +0 -184
  96. package/examples/registry-playbook-email-compositor-2.koi +0 -140
  97. package/examples/sentiment.koi +0 -90
  98. package/examples/simple.koi +0 -48
  99. package/examples/task-chaining-demo.koi +0 -244
  100. package/examples/test-await.koi +0 -22
  101. package/examples/test-crypto-sha256.koi +0 -196
  102. package/examples/test-delegation.koi +0 -41
  103. package/examples/test-multi-team-routing.koi +0 -258
  104. package/examples/test-no-handler.koi +0 -35
  105. package/examples/test-npm-import.koi +0 -67
  106. package/examples/test-parse.koi +0 -10
  107. package/examples/test-peers-with-team.koi +0 -59
  108. package/examples/test-permissions-fail.koi +0 -20
  109. package/examples/test-permissions.koi +0 -36
  110. package/examples/test-simple-registry.koi +0 -31
  111. package/examples/test-typescript-import.koi +0 -64
  112. package/examples/test-uses-team-syntax.koi +0 -25
  113. package/examples/test-uses-team.koi +0 -31
@@ -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 args = context.args || {};
197
- const state = context.state || {};
198
- // Use Function constructor to evaluate template string
199
- // This allows ${args.url}, ${state.foo}, etc. to be interpolated
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
- if (resolvedAction.condition !== undefined) {
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; // Skip this action
366
+ return;
244
367
  }
245
368
  }
246
369
 
247
- const intent = resolvedAction.intent || resolvedAction.type || resolvedAction.description;
248
- const actionTitle = resolvedAction.title || intent;
249
- cliLogger.progress(`[${this.name}] ${actionTitle}`);
370
+ cliLogger.planning(buildActionDisplay(this.name, resolvedAction));
250
371
 
251
- let result;
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
- const resultForContext = JSON.parse(JSON.stringify(result));
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 // Pass callback for incremental execution
427
+ onStreamAction,
428
+ memory
324
429
  );
325
430
 
326
- // If streaming was used, actions were already executed
327
431
  if (streamedActions.length > 0) {
328
- // Actions already executed via streaming - return final result from context
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 (resolvedAction.condition !== undefined) {
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}] ${actionTitle}`);
459
-
460
- // Check if this is a delegation action
461
- if (action.actionType === 'delegate') {
462
- // Delegation: route to appropriate team member
463
- if (process.env.KOI_DEBUG_LLM) {
464
- console.error(`[Agent:${this.name}] 🔀 Delegating action: ${action.intent}`);
465
- }
466
- finalResult = await this.resolveAction(resolvedAction, context);
467
- } else {
468
- // Direct action: check if this is a registered action with an executor
469
- const actionDef = actionRegistry.get(action.intent || action.type);
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 (a1, a2, a3, etc.)
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 (key.match(/^a\d+$/)) { // Match action IDs like a1, a2, a3...
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 string for template interpolation
777
- return typeof result === 'object' ? JSON.stringify(result) : String(result);
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.progress(` → [${this.name}] skill:${matchingSkill}...`);
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
- // Filter to only include agents that are in accessible teams
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
- const teamMemberNames = Object.keys(team.members);
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
- // Remove template variables for cleaner description
1145
- description = firstLine.replace(/\$\{[^}]+\}/g, '...').substring(0, 80);
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
- // Try to extract return structure from playbook
1151
- // Look for patterns like "return: { ... }" or "2. Return: { ... }"
1152
- for (const line of lines) {
1153
- const returnMatch = line.match(/(?:return|Return):\s*\{([^}]+)\}/i);
1154
- if (returnMatch) {
1155
- // Found a return statement - extract key structure
1156
- const returnContent = returnMatch[1];
1157
- // Extract field names (simple parsing)
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
- doc += `- { "actionType": "delegate", "intent": "${cap.intent}", "data": ... } - ${cap.description} (Delegate to ${cap.agent} [${cap.role}])\n`;
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, input) {
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
- // In a real implementation, this would look up and execute the skill
1335
- // For now, we'll simulate it
1336
- // Skill processing
1337
- return { success: true, skill: skillName, input };
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
  }