@koi-language/koi 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. package/QUICKSTART.md +89 -0
  2. package/README.md +545 -0
  3. package/examples/actions-demo.koi +177 -0
  4. package/examples/cache-test.koi +29 -0
  5. package/examples/calculator.koi +61 -0
  6. package/examples/clear-registry.js +33 -0
  7. package/examples/clear-registry.koi +30 -0
  8. package/examples/code-introspection-test.koi +149 -0
  9. package/examples/counter.koi +132 -0
  10. package/examples/delegation-test.koi +52 -0
  11. package/examples/directory-import-test.koi +84 -0
  12. package/examples/hello-world-claude.koi +52 -0
  13. package/examples/hello-world.koi +52 -0
  14. package/examples/hello.koi +24 -0
  15. package/examples/mcp-example.koi +70 -0
  16. package/examples/multi-event-handler-test.koi +144 -0
  17. package/examples/new-import-test.koi +89 -0
  18. package/examples/pipeline.koi +162 -0
  19. package/examples/registry-demo.koi +184 -0
  20. package/examples/registry-playbook-demo.koi +162 -0
  21. package/examples/registry-playbook-email-compositor-2.koi +140 -0
  22. package/examples/registry-playbook-email-compositor.koi +140 -0
  23. package/examples/sentiment.koi +90 -0
  24. package/examples/simple.koi +48 -0
  25. package/examples/skill-import-test.koi +76 -0
  26. package/examples/skills/advanced/index.koi +95 -0
  27. package/examples/skills/math-operations.koi +69 -0
  28. package/examples/skills/string-operations.koi +56 -0
  29. package/examples/task-chaining-demo.koi +244 -0
  30. package/examples/test-await.koi +22 -0
  31. package/examples/test-crypto-sha256.koi +196 -0
  32. package/examples/test-delegation.koi +41 -0
  33. package/examples/test-multi-team-routing.koi +258 -0
  34. package/examples/test-no-handler.koi +35 -0
  35. package/examples/test-npm-import.koi +67 -0
  36. package/examples/test-parse.koi +10 -0
  37. package/examples/test-peers-with-team.koi +59 -0
  38. package/examples/test-permissions-fail.koi +20 -0
  39. package/examples/test-permissions.koi +36 -0
  40. package/examples/test-simple-registry.koi +31 -0
  41. package/examples/test-typescript-import.koi +64 -0
  42. package/examples/test-uses-team-syntax.koi +25 -0
  43. package/examples/test-uses-team.koi +31 -0
  44. package/examples/utils/calculator.test.ts +144 -0
  45. package/examples/utils/calculator.ts +56 -0
  46. package/examples/utils/math-helpers.js +50 -0
  47. package/examples/utils/math-helpers.ts +55 -0
  48. package/examples/web-delegation-demo.koi +165 -0
  49. package/package.json +78 -0
  50. package/src/cli/koi.js +793 -0
  51. package/src/compiler/build-optimizer.js +447 -0
  52. package/src/compiler/cache-manager.js +274 -0
  53. package/src/compiler/import-resolver.js +369 -0
  54. package/src/compiler/parser.js +7542 -0
  55. package/src/compiler/transpiler.js +1105 -0
  56. package/src/compiler/typescript-transpiler.js +148 -0
  57. package/src/grammar/koi.pegjs +767 -0
  58. package/src/runtime/action-registry.js +172 -0
  59. package/src/runtime/actions/call-skill.js +45 -0
  60. package/src/runtime/actions/format.js +115 -0
  61. package/src/runtime/actions/print.js +42 -0
  62. package/src/runtime/actions/registry-delete.js +37 -0
  63. package/src/runtime/actions/registry-get.js +37 -0
  64. package/src/runtime/actions/registry-keys.js +33 -0
  65. package/src/runtime/actions/registry-search.js +34 -0
  66. package/src/runtime/actions/registry-set.js +50 -0
  67. package/src/runtime/actions/return.js +31 -0
  68. package/src/runtime/actions/send-message.js +58 -0
  69. package/src/runtime/actions/update-state.js +36 -0
  70. package/src/runtime/agent.js +1368 -0
  71. package/src/runtime/cli-logger.js +205 -0
  72. package/src/runtime/incremental-json-parser.js +201 -0
  73. package/src/runtime/index.js +33 -0
  74. package/src/runtime/llm-provider.js +1372 -0
  75. package/src/runtime/mcp-client.js +1171 -0
  76. package/src/runtime/planner.js +273 -0
  77. package/src/runtime/registry-backends/keyv-sqlite.js +215 -0
  78. package/src/runtime/registry-backends/local.js +260 -0
  79. package/src/runtime/registry.js +162 -0
  80. package/src/runtime/role.js +14 -0
  81. package/src/runtime/router.js +395 -0
  82. package/src/runtime/runtime.js +113 -0
  83. package/src/runtime/skill-selector.js +173 -0
  84. package/src/runtime/skill.js +25 -0
  85. package/src/runtime/team.js +162 -0
@@ -0,0 +1,1368 @@
1
+ import { LLMProvider } from './llm-provider.js';
2
+ import { cliLogger } from './cli-logger.js';
3
+ import { actionRegistry } from './action-registry.js';
4
+
5
+ // Global call stack to detect infinite loops across all agents
6
+ const globalCallStack = [];
7
+
8
+ export class Agent {
9
+ constructor(config) {
10
+ this.name = config.name;
11
+ this.role = config.role;
12
+ this.skills = config.skills || [];
13
+ this.usesTeams = config.usesTeams || []; // Teams this agent uses as a client
14
+ this.llm = config.llm || { provider: 'openai', model: 'gpt-4', temperature: 0.2 };
15
+ this.state = config.state || {};
16
+ this.playbooks = config.playbooks || {};
17
+ this.resilience = config.resilience || null;
18
+
19
+ // Never allow peers to be null - use a proxy that throws helpful error
20
+ if (config.peers) {
21
+ this.peers = config.peers;
22
+ } else {
23
+ this.peers = this._createNoTeamProxy();
24
+ }
25
+
26
+ this.handlers = config.handlers || {};
27
+
28
+ // Initialize LLM provider if needed
29
+ this.llmProvider = null;
30
+ }
31
+
32
+ /**
33
+ * Create a proxy that throws a helpful error when trying to use peers without a team
34
+ */
35
+ _createNoTeamProxy() {
36
+ // Return an object that mimics a Team but throws when execute() is called
37
+ let eventName = 'unknown';
38
+ const noTeamQuery = {
39
+ __isNoTeamProxy: true, // Marker for Team constructor to detect and replace
40
+ event: (name) => {
41
+ eventName = name;
42
+ return noTeamQuery;
43
+ },
44
+ role: () => noTeamQuery,
45
+ any: () => noTeamQuery,
46
+ all: () => noTeamQuery,
47
+ execute: () => {
48
+ throw new Error(`NO_AGENT_HANDLER:${eventName}::no-team`);
49
+ }
50
+ };
51
+ return noTeamQuery;
52
+ }
53
+
54
+ /**
55
+ * Get a specific team by reference (for peers(TeamName) syntax)
56
+ * @param {Team} teamRef - Team instance or constructor
57
+ * @returns {Team} The team instance
58
+ */
59
+ _getTeam(teamRef) {
60
+ // If teamRef is already a Team instance, check if we have access to it
61
+ if (teamRef && typeof teamRef === 'object') {
62
+ // Check if it's the same instance as peers
63
+ if (this.peers === teamRef) {
64
+ return this.peers;
65
+ }
66
+
67
+ // Search in usesTeams array for the exact same instance
68
+ if (Array.isArray(this.usesTeams)) {
69
+ const team = this.usesTeams.find(t => t === teamRef);
70
+ if (team) {
71
+ return team;
72
+ }
73
+ }
74
+
75
+ // Team not found - throw helpful error
76
+ const teamName = teamRef.name || teamRef.constructor?.name || 'Unknown';
77
+ throw new Error(
78
+ `Agent ${this.name} does not have access to team ${teamName}.\n` +
79
+ `Available teams: ${this.usesTeams.map(t => t?.name || t?.constructor?.name || 'Unknown').join(', ') || 'none'}\n` +
80
+ `Hint: Add "uses Team ${teamName}" to the agent definition.`
81
+ );
82
+ }
83
+
84
+ // If teamRef is a constructor/class, search by constructor
85
+ if (typeof teamRef === 'function') {
86
+ if (this.peers && this.peers.constructor === teamRef) {
87
+ return this.peers;
88
+ }
89
+
90
+ if (Array.isArray(this.usesTeams)) {
91
+ const team = this.usesTeams.find(t => t && t.constructor === teamRef);
92
+ if (team) {
93
+ return team;
94
+ }
95
+ }
96
+ }
97
+
98
+ throw new Error(
99
+ `Agent ${this.name} could not find team.\n` +
100
+ `Available teams: ${this.usesTeams.map(t => t?.name || t?.constructor?.name || 'Unknown').join(', ') || 'none'}`
101
+ );
102
+ }
103
+
104
+ /**
105
+ * Check if the agent has a specific permission
106
+ * Supports hierarchical permissions: if role has "registry", it can execute "registry:read", "registry:write", etc.
107
+ * @param {string} permissionName - Permission to check (e.g., 'execute', 'delegate', 'registry:read')
108
+ * @returns {boolean} True if agent has the permission
109
+ */
110
+ hasPermission(permissionName) {
111
+ if (!this.role) {
112
+ return false;
113
+ }
114
+
115
+ // Check exact permission match first
116
+ if (this.role.can(permissionName)) {
117
+ return true;
118
+ }
119
+
120
+ // Check hierarchical permissions (e.g., "registry" covers "registry:read")
121
+ if (permissionName.includes(':')) {
122
+ const [prefix] = permissionName.split(':');
123
+ if (this.role.can(prefix)) {
124
+ return true;
125
+ }
126
+ }
127
+
128
+ return false;
129
+ }
130
+
131
+ async handle(eventName, args, _fromDelegation = false) {
132
+ if (!_fromDelegation) {
133
+ cliLogger.progress(`[🤖 ${this.name}] ${eventName}...`);
134
+ }
135
+
136
+ const handler = this.handlers[eventName];
137
+ if (!handler) {
138
+ cliLogger.clear();
139
+ cliLogger.error(`[🤖 ${this.name}] No handler for event: ${eventName}`);
140
+ throw new Error(`Agent ${this.name} has no handler for event: ${eventName}`);
141
+ }
142
+
143
+ try {
144
+ // Check if handler is playbook-only (has __playbookOnly__ flag)
145
+ if (handler.__playbookOnly__) {
146
+ const result = await this.executePlaybookHandler(eventName, handler.__playbook__, args, _fromDelegation);
147
+ cliLogger.clear();
148
+ return result;
149
+ }
150
+
151
+ // Execute handler with agent context
152
+ const result = await handler.call(this, args);
153
+ cliLogger.clear();
154
+ return result;
155
+ } catch (error) {
156
+ cliLogger.clear();
157
+ // Don't log NO_AGENT_HANDLER errors - they'll be handled in runtime.js
158
+ if (!error.message || !error.message.startsWith('NO_AGENT_HANDLER:')) {
159
+ cliLogger.error(`[${this.name}] Error in ${eventName}: ${error.message}`);
160
+
161
+ // Apply resilience if configured
162
+ if (this.resilience?.retry_max_attempts) {
163
+ console.log(`[Agent:${this.name}] Applying resilience policy...`);
164
+ // TODO: Implement retry logic
165
+ }
166
+ }
167
+
168
+ throw error;
169
+ }
170
+ }
171
+
172
+ async executePlaybookHandler(eventName, playbook, args, _fromDelegation = false) {
173
+ // Initialize LLM provider if not already done
174
+ if (!this.llmProvider) {
175
+ this.llmProvider = new LLMProvider(this.llm);
176
+ }
177
+
178
+ // Prepare context with args and state
179
+ const context = {
180
+ args,
181
+ state: this.state
182
+ };
183
+
184
+ // Get available skill functions for tool calling
185
+ const tools = this.getSkillFunctions();
186
+
187
+ // Extract playbook content if it's an object (transpiler stores it as {type, content})
188
+ const playbookContent = typeof playbook === 'object' && playbook.content
189
+ ? playbook.content
190
+ : playbook;
191
+
192
+ // Evaluate template string with context (interpolate ${...} expressions)
193
+ // Create a function that evaluates the template in the context of args and state
194
+ const evaluateTemplate = (template, context) => {
195
+ 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
200
+ const fn = new Function('args', 'state', `return \`${template}\`;`);
201
+ return fn(args, state);
202
+ } catch (error) {
203
+ console.warn(`[Agent:${this.name}] Failed to evaluate playbook template: ${error.message}`);
204
+ return template; // Return original if evaluation fails
205
+ }
206
+ };
207
+
208
+ const interpolatedPlaybook = evaluateTemplate(playbookContent, context);
209
+
210
+ // Use skillSelector for semantic skill selection instead of passing all skills
211
+ // This improves accuracy by only passing relevant tools to the LLM
212
+ let selectedTools = tools;
213
+ if (typeof globalThis.skillSelector !== 'undefined' && interpolatedPlaybook) {
214
+ try {
215
+ selectedTools = await globalThis.skillSelector.selectSkillsForTask(interpolatedPlaybook, 2);
216
+ } catch (error) {
217
+ console.warn(`[Agent:${this.name}] Skill selection failed, using all skills: ${error.message}`);
218
+ selectedTools = tools; // Fallback to all skills
219
+ }
220
+ }
221
+
222
+ // STREAMING OPTIMIZATION: Execute actions incrementally as they arrive
223
+ // This reduces latency by starting execution before the full JSON is received
224
+ const streamedActions = [];
225
+ const actionContext = {
226
+ state: this.state,
227
+ results: [],
228
+ args
229
+ };
230
+
231
+ // Callback for incremental action execution
232
+ const onStreamAction = async (action) => {
233
+ streamedActions.push(action);
234
+
235
+ // Execute action immediately (sequential execution for proper chaining)
236
+ try {
237
+ const resolvedAction = this.resolveActionReferences(action, actionContext);
238
+
239
+ // Check condition if present
240
+ if (resolvedAction.condition !== undefined) {
241
+ const conditionMet = this.evaluateCondition(resolvedAction.condition, actionContext);
242
+ if (!conditionMet) {
243
+ return; // Skip this action
244
+ }
245
+ }
246
+
247
+ const intent = resolvedAction.intent || resolvedAction.type || resolvedAction.description;
248
+ const actionTitle = resolvedAction.title || intent;
249
+ cliLogger.progress(`[${this.name}] ${actionTitle}`);
250
+
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
+ }
275
+
276
+ cliLogger.clear();
277
+
278
+ // Update context for next action (chaining)
279
+ if (result && typeof result === 'object') {
280
+ const resultForContext = JSON.parse(JSON.stringify(result));
281
+ actionContext.results.push(resultForContext);
282
+
283
+ // Store result with action ID for explicit referencing
284
+ if (action.id) {
285
+ actionContext[action.id] = { output: resultForContext };
286
+
287
+ if (process.env.KOI_DEBUG_LLM) {
288
+ const preview = JSON.stringify(resultForContext).substring(0, 200);
289
+ console.error(`[Agent:${this.name}] 💾 Stored ${action.id}.output = ${preview}`);
290
+ }
291
+ }
292
+
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
+ const nonDataActions = ['print', 'log', 'format'];
296
+
297
+ if (!nonDataActions.includes(intent)) {
298
+ actionContext.previousResult = resultForContext;
299
+ actionContext.lastResult = resultForContext;
300
+ }
301
+
302
+ Object.keys(resultForContext).forEach(key => {
303
+ if (!actionContext[key]) {
304
+ actionContext[key] = resultForContext[key];
305
+ }
306
+ });
307
+ }
308
+ } catch (error) {
309
+ cliLogger.clear();
310
+ console.error(`[${this.name}] Error executing streamed action: ${error.message}`);
311
+ throw error;
312
+ }
313
+ };
314
+
315
+ // Execute playbook with streaming (onAction callback receives each action as it completes)
316
+ const result = await this.llmProvider.executePlaybook(
317
+ interpolatedPlaybook,
318
+ context,
319
+ this.name,
320
+ selectedTools,
321
+ this,
322
+ _fromDelegation,
323
+ onStreamAction // Pass callback for incremental execution
324
+ );
325
+
326
+ // If streaming was used, actions were already executed
327
+ if (streamedActions.length > 0) {
328
+ // Actions already executed via streaming - return final result from context
329
+ const finalResult = actionContext.results.length > 0
330
+ ? actionContext.results[actionContext.results.length - 1]
331
+ : {};
332
+
333
+ if (actionContext.results.length > 1) {
334
+ return {
335
+ ...finalResult,
336
+ _allResults: actionContext.results,
337
+ _finalResult: finalResult
338
+ };
339
+ }
340
+
341
+ return finalResult;
342
+ }
343
+
344
+ // No streaming - handle traditional execution path
345
+ // Handle malformed responses - if LLM didn't return actions or result, try to extract
346
+ if (result && !result.actions && !result.result) {
347
+ // LLM returned unexpected format - try to find actions in other fields
348
+ for (const key of Object.keys(result)) {
349
+ if (Array.isArray(result[key]) && result[key].length > 0 && result[key][0].type) {
350
+ // Found array of actions under different key
351
+ console.warn(`[${this.name}] ⚠️ LLM returned actions under "${key}" instead of "actions" - fixing`);
352
+ result.actions = result[key];
353
+ delete result[key];
354
+ break;
355
+ }
356
+ }
357
+ }
358
+
359
+ // Check if LLM returned actions (new action-based system)
360
+ 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
+ const canDelegateToTeams = this.usesTeams && this.usesTeams.length > 0;
369
+ const shouldExecuteActions = !_fromDelegation || !canDelegateToTeams;
370
+
371
+ 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
+ const { actions, ...additionalFields } = result;
377
+
378
+ // Execute the actions
379
+ const actionResults = await this.executeActions(actions);
380
+
381
+ // If there are additional fields, merge them with the action results
382
+ if (Object.keys(additionalFields).length > 0) {
383
+ return {
384
+ ...additionalFields,
385
+ ...actionResults
386
+ };
387
+ }
388
+
389
+ return actionResults;
390
+ } 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
+ console.log(`[${this.name}] ⚠️ Ignoring nested actions (orchestrator in delegated call)`);
394
+
395
+ // Return the result without the actions field
396
+ const { actions, ...actualResult } = result;
397
+
398
+ // If there's no other data besides actions, try to extract from first action
399
+ if (Object.keys(actualResult).length === 0 && actions.length > 0) {
400
+ const firstAction = actions[0];
401
+ return firstAction.data || firstAction.result || {};
402
+ }
403
+
404
+ return actualResult;
405
+ }
406
+ }
407
+
408
+ // Legacy support: Apply state updates if returned by LLM
409
+ if (result && typeof result === 'object') {
410
+ // Check if LLM returned state updates
411
+ if (result.state_updates || result.stateUpdates) {
412
+ const updates = result.state_updates || result.stateUpdates;
413
+
414
+ // Apply updates to agent state
415
+ Object.keys(updates).forEach(key => {
416
+ this.state[key] = updates[key];
417
+ });
418
+ }
419
+
420
+ // If result has both state_updates and other fields, return just the result fields
421
+ if (result.state_updates || result.stateUpdates) {
422
+ const { state_updates, stateUpdates, ...resultData } = result;
423
+ return resultData;
424
+ }
425
+ }
426
+
427
+ return result;
428
+ }
429
+
430
+ async executeActions(actions) {
431
+ let finalResult = {};
432
+ let context = {
433
+ state: this.state,
434
+ results: [] // Track all results for chaining
435
+ };
436
+
437
+ for (let i = 0; i < actions.length; i++) {
438
+ const action = actions[i];
439
+
440
+ // Resolve variable references FIRST (before condition check)
441
+ const resolvedAction = this.resolveActionReferences(action, context);
442
+
443
+ // Check if action has a condition - skip if condition is false
444
+ // IMPORTANT: Evaluate condition BEFORE the action executes, using current context
445
+ if (resolvedAction.condition !== undefined) {
446
+ const conditionMet = this.evaluateCondition(resolvedAction.condition, context);
447
+ if (!conditionMet) {
448
+ // Skip this action silently
449
+ continue;
450
+ }
451
+ }
452
+
453
+ const intent = resolvedAction.intent || resolvedAction.type || resolvedAction.description;
454
+
455
+ // Show what action is being executed
456
+ // Use the "title" field if the LLM provided one, otherwise use intent
457
+ 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
+ }
489
+ }
490
+
491
+ // Clear progress after action completes
492
+ cliLogger.clear();
493
+
494
+ if (process.env.KOI_DEBUG_LLM) {
495
+ console.error(`[Agent:${this.name}] 🔍 Action ${intent} returned:`, JSON.stringify(finalResult).substring(0, 150));
496
+ }
497
+
498
+ // Update context with result for next action (chaining)
499
+ if (finalResult && typeof finalResult === 'object') {
500
+ // Unwrap double-encoded results (LLM sometimes returns { "result": "{...json...}" })
501
+ if (finalResult.result && typeof finalResult.result === 'string' &&
502
+ Object.keys(finalResult).length === 1) {
503
+ try {
504
+ const parsed = JSON.parse(finalResult.result);
505
+ if (typeof parsed === 'object') {
506
+ finalResult = parsed;
507
+ }
508
+ } catch (e) {
509
+ // Not JSON, keep as-is
510
+ }
511
+ }
512
+
513
+ // Deep clone result to avoid reference issues with conditions
514
+ const resultForContext = JSON.parse(JSON.stringify(finalResult));
515
+
516
+ context.results.push(resultForContext);
517
+
518
+ // Store result with action ID for explicit referencing
519
+ if (action.id) {
520
+ context[action.id] = { output: resultForContext };
521
+
522
+ if (process.env.KOI_DEBUG_LLM) {
523
+ console.error(`[Agent:${this.name}] 💾 Stored ${action.id}.output`);
524
+ }
525
+ }
526
+
527
+ // Only update previousResult for actions that produce meaningful data
528
+ // Side-effect actions like print, format (that return metadata) should not override previousResult
529
+ const nonDataActions = ['print', 'log', 'format'];
530
+
531
+ if (!nonDataActions.includes(intent)) {
532
+ context.previousResult = resultForContext; // Last meaningful result
533
+ context.lastResult = resultForContext; // Alias
534
+
535
+ if (process.env.KOI_DEBUG_LLM) {
536
+ console.error(`[Agent:${this.name}] 📌 Updated previousResult from ${intent}:`, JSON.stringify(context.previousResult).substring(0, 100));
537
+ }
538
+ } else {
539
+ if (process.env.KOI_DEBUG_LLM) {
540
+ console.error(`[Agent:${this.name}] 🚫 NOT updating previousResult for ${intent} (side-effect action)`);
541
+ }
542
+ }
543
+
544
+ // Make result fields directly accessible
545
+ Object.keys(resultForContext).forEach(key => {
546
+ if (!context[key]) { // Don't override system fields
547
+ context[key] = resultForContext[key];
548
+ }
549
+ });
550
+ }
551
+ }
552
+
553
+ // If multiple actions were executed, return complete context with all results
554
+ if (context.results.length > 1) {
555
+ return {
556
+ ...finalResult, // Include final result fields at top level for backward compatibility
557
+ _allResults: context.results, // Full chain of results
558
+ _finalResult: finalResult // Explicit final result
559
+ };
560
+ }
561
+
562
+ // Single action - just return the result
563
+ return finalResult;
564
+ }
565
+
566
+
567
+ /**
568
+ * Resolve variable references in action data
569
+ * Supports: ${a1.output.field}, ${previousResult.field}, ${results[0].field}, ${field}
570
+ */
571
+ resolveActionReferences(action, context) {
572
+ // Deep clone to avoid mutating original
573
+ const resolved = JSON.parse(JSON.stringify(action));
574
+
575
+ if (process.env.KOI_DEBUG_LLM) {
576
+ console.error(`[Agent:${this.name}] 🔄 Resolving references for action: ${action.intent || action.type}`);
577
+ }
578
+
579
+ // DON'T resolve condition here - it will be evaluated directly in evaluateCondition()
580
+ // (Conditions need special handling to preserve boolean expressions)
581
+
582
+ // Resolve references in data field
583
+ if (resolved.data) {
584
+ resolved.data = this.resolveObjectReferences(resolved.data, context);
585
+ }
586
+
587
+ // Resolve references in input field
588
+ if (resolved.input) {
589
+ resolved.input = this.resolveObjectReferences(resolved.input, context);
590
+ }
591
+
592
+ // Resolve references in key, value, query, prefix fields (for registry operations)
593
+ if (resolved.key) {
594
+ resolved.key = this.resolveObjectReferences(resolved.key, context);
595
+ }
596
+ if (resolved.value !== undefined) {
597
+ resolved.value = this.resolveObjectReferences(resolved.value, context);
598
+ }
599
+ if (resolved.query) {
600
+ resolved.query = this.resolveObjectReferences(resolved.query, context);
601
+ }
602
+ if (resolved.prefix !== undefined) {
603
+ resolved.prefix = this.resolveObjectReferences(resolved.prefix, context);
604
+ }
605
+
606
+ // Resolve references in message/text fields (for print action)
607
+ if (resolved.message !== undefined) {
608
+ resolved.message = this.resolveObjectReferences(resolved.message, context);
609
+ }
610
+ if (resolved.text !== undefined) {
611
+ resolved.text = this.resolveObjectReferences(resolved.text, context);
612
+ }
613
+
614
+ return resolved;
615
+ }
616
+
617
+ /**
618
+ * Build evaluation context with all available variables including action IDs
619
+ */
620
+ buildEvalContext(context) {
621
+ const evalContext = {
622
+ previousResult: context.previousResult,
623
+ lastResult: context.lastResult,
624
+ results: context.results,
625
+ state: context.state,
626
+ args: context.args,
627
+ Date, // Allow Date constructor
628
+ JSON, // Allow JSON methods
629
+ Math // Allow Math methods
630
+ };
631
+ // Add all action IDs from context (a1, a2, a3, etc.)
632
+ for (const key in context) {
633
+ if (key.match(/^a\d+$/)) { // Match action IDs like a1, a2, a3...
634
+ evalContext[key] = context[key];
635
+ }
636
+ }
637
+ return evalContext;
638
+ }
639
+
640
+ /**
641
+ * Recursively resolve references in an object
642
+ */
643
+ resolveObjectReferences(obj, context) {
644
+ if (typeof obj === 'string') {
645
+ // Check if the ENTIRE string is a single ${...} reference (not a template)
646
+ const singleRefMatch = obj.match(/^\$\{([^}]+)\}$/);
647
+ if (singleRefMatch) {
648
+ const expr = singleRefMatch[1].trim();
649
+
650
+ // Try to get it as a direct path from context
651
+ const directValue = this.getNestedValue(context, expr);
652
+ if (directValue !== undefined) {
653
+ // Return the value directly (could be object, array, number, etc.)
654
+ return directValue;
655
+ }
656
+
657
+ // Try to evaluate as JavaScript expression
658
+ try {
659
+ const evalContext = this.buildEvalContext(context);
660
+ const fn = new Function(...Object.keys(evalContext), `return ${expr};`);
661
+ const result = fn(...Object.values(evalContext));
662
+ return result !== undefined ? result : obj;
663
+ } catch (error) {
664
+ return obj;
665
+ }
666
+ }
667
+
668
+ // Multiple references in a template string - resolve to a string
669
+ return obj.replace(/\$\{([^}]+)\}/g, (match, expr) => {
670
+ const trimmedExpr = expr.trim();
671
+
672
+ // First try to get it as a direct path from context
673
+ const directValue = this.getNestedValue(context, trimmedExpr);
674
+ if (directValue !== undefined) {
675
+ // Convert to string for template interpolation
676
+ return typeof directValue === 'object' ? JSON.stringify(directValue) : String(directValue);
677
+ }
678
+
679
+ // Log only unresolved placeholders in debug mode
680
+ if (process.env.KOI_DEBUG_LLM) {
681
+ console.error(`[Agent:${this.name}] ⚠️ Could not resolve placeholder: ${trimmedExpr}`);
682
+ }
683
+
684
+ // If not found in context, try to evaluate as JavaScript expression
685
+ try {
686
+ const evalContext = this.buildEvalContext(context);
687
+ const fn = new Function(...Object.keys(evalContext), `return ${trimmedExpr};`);
688
+ const result = fn(...Object.values(evalContext));
689
+ return result !== undefined ? (typeof result === 'object' ? JSON.stringify(result) : String(result)) : match;
690
+ } catch (error) {
691
+ // If evaluation fails, return the original match
692
+ return match;
693
+ }
694
+ });
695
+ }
696
+
697
+ if (Array.isArray(obj)) {
698
+ return obj.map(item => this.resolveObjectReferences(item, context));
699
+ }
700
+
701
+ if (obj && typeof obj === 'object') {
702
+ const resolved = {};
703
+ for (const [key, value] of Object.entries(obj)) {
704
+ resolved[key] = this.resolveObjectReferences(value, context);
705
+ }
706
+ return resolved;
707
+ }
708
+
709
+ return obj;
710
+ }
711
+
712
+ /**
713
+ * Get nested value from object using dot notation
714
+ * Examples: "previousResult.translated", "results[0].count"
715
+ */
716
+ getNestedValue(obj, path) {
717
+ // Handle array access: results[0].field
718
+ path = path.replace(/\[(\d+)\]/g, '.$1');
719
+
720
+ const parts = path.split('.');
721
+ let current = obj;
722
+
723
+ for (const part of parts) {
724
+ if (current === undefined || current === null) {
725
+ return undefined;
726
+ }
727
+ current = current[part];
728
+ }
729
+
730
+ return current;
731
+ }
732
+
733
+ /**
734
+ * Evaluate a condition expression
735
+ * Supports: boolean values, comparison expressions with context variables
736
+ * Examples: true, false, "${previousResult.found}", "${previousResult.count > 0}"
737
+ */
738
+ evaluateCondition(condition, context) {
739
+ // If already a boolean, return it
740
+ if (typeof condition === 'boolean') {
741
+ return condition;
742
+ }
743
+
744
+ // If it's an object, convert to string first (LLM sometimes sends objects)
745
+ if (typeof condition === 'object' && condition !== null) {
746
+ console.warn(`[Agent:${this.name}] Condition is an object, converting to string: ${JSON.stringify(condition)}`);
747
+ return false; // Skip actions with malformed conditions
748
+ }
749
+
750
+ // If it's a string, evaluate it as JavaScript expression
751
+ if (typeof condition === 'string') {
752
+ // Check if the entire condition is a single ${...} expression
753
+ const singleExprMatch = condition.match(/^\$\{([^}]+)\}$/);
754
+ if (singleExprMatch) {
755
+ // Evaluate the expression directly and return its boolean value
756
+ try {
757
+ const expr = singleExprMatch[1].trim();
758
+ const evalContext = { ...this.buildEvalContext(context), ...context };
759
+ // Evaluate the expression and convert to boolean
760
+ // Don't use !! here because the expression itself might contain negations
761
+ const fn = new Function(...Object.keys(evalContext), `return ${expr};`);
762
+ const rawResult = fn(...Object.values(evalContext));
763
+ return !!rawResult;
764
+ } catch (error) {
765
+ console.warn(`[Agent:${this.name}] Failed to evaluate condition expression "${condition}": ${error.message}`);
766
+ return false;
767
+ }
768
+ }
769
+
770
+ // Multiple ${...} expressions or mixed content - resolve then evaluate
771
+ let resolved = condition.replace(/\$\{([^}]+)\}/g, (match, expr) => {
772
+ try {
773
+ const evalContext = { ...this.buildEvalContext(context), ...context };
774
+ const fn = new Function(...Object.keys(evalContext), `return ${expr};`);
775
+ const result = fn(...Object.values(evalContext));
776
+ // Convert to string for template interpolation
777
+ return typeof result === 'object' ? JSON.stringify(result) : String(result);
778
+ } catch (error) {
779
+ console.warn(`[Agent:${this.name}] Failed to evaluate condition sub-expression "${expr}": ${error.message}`);
780
+ return match;
781
+ }
782
+ });
783
+
784
+ // Try to evaluate the resolved string as a boolean
785
+ if (resolved === 'true') return true;
786
+ if (resolved === 'false') return false;
787
+
788
+ try {
789
+ const evalContext = { ...this.buildEvalContext(context), ...context };
790
+ const fn = new Function(...Object.keys(evalContext), `return !!(${resolved});`);
791
+ return fn(...Object.values(evalContext));
792
+ } catch (error) {
793
+ console.warn(`[Agent:${this.name}] Failed to evaluate resolved condition "${resolved}": ${error.message}`);
794
+ return false;
795
+ }
796
+ }
797
+
798
+ // Default to false for unknown types
799
+ return false;
800
+ }
801
+
802
+ /**
803
+ * Resolve an action using cascading strategy:
804
+ * 1️⃣ Can I handle it myself (do I have a handler)?
805
+ * 2️⃣ Do I have a skill that can do it?
806
+ * 3️⃣ Can I delegate to another agent via router?
807
+ * 4️⃣ Can I execute directly with a simple prompt?
808
+ */
809
+ async resolveAction(action, context = {}) {
810
+ const intent = action.intent || action.type || action.description;
811
+
812
+ // Check for infinite loops before proceeding
813
+ const callSignature = `${this.name}:${intent}`;
814
+ if (globalCallStack.includes(callSignature)) {
815
+ throw new Error(
816
+ `[Agent:${this.name}] Infinite loop detected!\n` +
817
+ ` Call stack: ${globalCallStack.join(' → ')} → ${callSignature}\n` +
818
+ ` Preventing recursion for intent: "${intent}"`
819
+ );
820
+ }
821
+
822
+ // Push to call stack
823
+ globalCallStack.push(callSignature);
824
+
825
+ try {
826
+ // 1️⃣ Do I have a handler for this? (check my own event handlers)
827
+ const matchingHandler = this.findMatchingHandler(intent);
828
+ if (matchingHandler) {
829
+ // Self-delegation (same agent handles it)
830
+ const result = await this.handle(matchingHandler, action.data || action.input || {}, false);
831
+ globalCallStack.pop();
832
+ return result;
833
+ }
834
+
835
+ // 2️⃣ Do I have a matching skill?
836
+ const matchingSkill = this.findMatchingSkill(intent);
837
+ if (matchingSkill) {
838
+ cliLogger.progress(` → [${this.name}] skill:${matchingSkill}...`);
839
+ const result = await this.callSkill(matchingSkill, action.data || action.input || {});
840
+ cliLogger.clear();
841
+ globalCallStack.pop();
842
+ return result;
843
+ }
844
+
845
+ // 3️⃣ Can someone in my teams handle it? (check peers + usesTeams)
846
+ if (this.peers || this.usesTeams.length > 0) {
847
+ // Search within team members - team defines communication boundaries
848
+ const teamMember = await this.findTeamMemberForIntent(intent);
849
+
850
+ if (teamMember) {
851
+ // Show delegation with indentation
852
+ const actionTitle = action.title || intent;
853
+ cliLogger.pushIndent(`[${teamMember.agent.name}] ${actionTitle}`);
854
+
855
+ const result = await teamMember.agent.handle(teamMember.event, action.data || action.input || {}, true);
856
+
857
+ // Pop indentation when delegation returns
858
+ cliLogger.popIndent();
859
+
860
+ globalCallStack.pop();
861
+ return result;
862
+ }
863
+ } else if (intent && typeof intent === 'string' && intent.trim() !== '') {
864
+ // No teams defined - fall back to global router (rare case)
865
+ const { agentRouter } = await import('./router.js');
866
+ let matches = await agentRouter.findMatches(intent, 5);
867
+
868
+ // Filter out self-delegation
869
+ matches = matches.filter(match => match.agent !== this);
870
+
871
+ if (matches.length > 0) {
872
+ const best = matches[0];
873
+ const actionTitle = action.title || intent;
874
+ cliLogger.pushIndent(`[${best.agent.name}] ${actionTitle}`);
875
+
876
+ const result = await best.agent.handle(best.event, action.data || action.input || {}, true);
877
+ cliLogger.popIndent();
878
+
879
+ globalCallStack.pop();
880
+ return result;
881
+ }
882
+ }
883
+
884
+ // 4️⃣ Can I execute directly with LLM? (simple tasks, only if no one else can do it)
885
+ if (this.canExecuteDirectly(action)) {
886
+ const result = await this.executeDirectly(action, context);
887
+ globalCallStack.pop();
888
+ return result;
889
+ }
890
+
891
+ // ❌ Cannot resolve
892
+ globalCallStack.pop();
893
+ throw new Error(
894
+ `[Agent:${this.name}] Cannot resolve: "${intent}"\n` +
895
+ ` - I don't have a handler for this\n` +
896
+ ` - I don't have a matching skill\n` +
897
+ ` - No team member available via router\n` +
898
+ ` - Too complex for direct execution`
899
+ );
900
+ } catch (error) {
901
+ // Clean up call stack on error
902
+ globalCallStack.pop();
903
+ throw error;
904
+ }
905
+ }
906
+
907
+ /**
908
+ * Find a team member that can handle the intent
909
+ * Searches in peers (if member of a team) and usesTeams (teams this agent uses)
910
+ */
911
+ async findTeamMemberForIntent(intent) {
912
+ if (!intent || typeof intent !== 'string' || intent.trim() === '') {
913
+ return null;
914
+ }
915
+
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
+ // Collect all teams this agent can access
922
+ const accessibleTeams = [];
923
+
924
+ // Add peers team (if this agent is a member of a team)
925
+ if (this.peers && this.peers.members) {
926
+ accessibleTeams.push(this.peers);
927
+ }
928
+
929
+ // Add usesTeams (teams this agent uses as a client)
930
+ for (const team of this.usesTeams) {
931
+ if (team && team.members) {
932
+ accessibleTeams.push(team);
933
+ }
934
+ }
935
+
936
+ if (accessibleTeams.length === 0) {
937
+ return null;
938
+ }
939
+
940
+ // Filter to only include agents that are in accessible teams
941
+ matches = matches.filter(match => {
942
+ // Check if this agent is in any accessible team
943
+ const isAccessible = accessibleTeams.some(team => {
944
+ const teamMemberNames = Object.keys(team.members);
945
+ return teamMemberNames.some(name => {
946
+ const member = team.members[name];
947
+ return member === match.agent || member.name === match.agent.name;
948
+ });
949
+ });
950
+
951
+ // Also exclude self
952
+ return isAccessible && match.agent !== this;
953
+ });
954
+
955
+ if (matches.length > 0) {
956
+ return matches[0];
957
+ }
958
+
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
+ return null;
974
+ }
975
+
976
+ /**
977
+ * Find a handler in this agent that matches the intent
978
+ */
979
+ findMatchingHandler(intent) {
980
+ if (!this.handlers || Object.keys(this.handlers).length === 0) {
981
+ return null;
982
+ }
983
+
984
+ if (!intent || typeof intent !== 'string') {
985
+ return null;
986
+ }
987
+
988
+ const intentLower = intent.toLowerCase().replace(/[^a-z0-9]/g, ''); // Remove non-alphanumeric
989
+
990
+ // Try exact match first (case insensitive, ignoring separators)
991
+ for (const eventName of Object.keys(this.handlers)) {
992
+ const eventNormalized = eventName.toLowerCase().replace(/[^a-z0-9]/g, '');
993
+ if (eventNormalized === intentLower) {
994
+ return eventName;
995
+ }
996
+ }
997
+
998
+ // Try partial match
999
+ for (const eventName of Object.keys(this.handlers)) {
1000
+ const eventLower = eventName.toLowerCase();
1001
+ const intentOriginal = intent.toLowerCase();
1002
+
1003
+ if (intentOriginal.includes(eventLower) || eventLower.includes(intentOriginal)) {
1004
+ return eventName;
1005
+ }
1006
+ }
1007
+
1008
+ // Try keyword matching (split by spaces and camelCase)
1009
+ const keywords = intent
1010
+ .replace(/([a-z])([A-Z])/g, '$1 $2') // Split camelCase
1011
+ .toLowerCase()
1012
+ .split(/\s+/)
1013
+ .filter(k => k.length > 2);
1014
+
1015
+ for (const eventName of Object.keys(this.handlers)) {
1016
+ const eventLower = eventName.toLowerCase();
1017
+
1018
+ for (const keyword of keywords) {
1019
+ if (eventLower.includes(keyword)) {
1020
+ return eventName;
1021
+ }
1022
+ }
1023
+ }
1024
+
1025
+ return null;
1026
+ }
1027
+
1028
+ /**
1029
+ * Generate documentation of peer capabilities for LLM prompts
1030
+ * Returns a string describing what intents can be delegated to which peers
1031
+ */
1032
+ getPeerCapabilitiesDocumentation() {
1033
+ const capabilities = [];
1034
+ const processedAgents = new Set();
1035
+
1036
+ // Helper function to collect handlers from an agent
1037
+ const collectHandlers = (agent, teamName = null) => {
1038
+ if (!agent || processedAgents.has(agent.name)) {
1039
+ return;
1040
+ }
1041
+ processedAgents.add(agent.name);
1042
+
1043
+ if (agent.handlers && Object.keys(agent.handlers).length > 0) {
1044
+ const handlers = Object.keys(agent.handlers);
1045
+ const agentInfo = teamName ? `${agent.name} (${teamName})` : agent.name;
1046
+
1047
+ // Collect handler details with descriptions
1048
+ const handlerDetails = [];
1049
+ for (const handler of handlers) {
1050
+ const handlerFn = agent.handlers[handler];
1051
+ let description = '';
1052
+
1053
+ if (handlerFn && handlerFn.__playbook__) {
1054
+ const playbook = handlerFn.__playbook__;
1055
+ const firstLine = playbook.split('\n')[0].trim();
1056
+ description = firstLine.replace(/\$\{[^}]+\}/g, '...').substring(0, 60);
1057
+ if (description.length < firstLine.length) {
1058
+ description += '...';
1059
+ }
1060
+ }
1061
+
1062
+ handlerDetails.push({
1063
+ name: handler,
1064
+ description: description || `Handle ${handler}`
1065
+ });
1066
+ }
1067
+
1068
+ capabilities.push({
1069
+ agent: agentInfo,
1070
+ role: agent.role ? agent.role.name : 'Unknown',
1071
+ handlers: handlerDetails
1072
+ });
1073
+ }
1074
+ };
1075
+
1076
+ // Collect from peers team (if this agent is a member of a team)
1077
+ if (this.peers && this.peers.members) {
1078
+ const memberNames = Object.keys(this.peers.members);
1079
+ for (const memberName of memberNames) {
1080
+ const member = this.peers.members[memberName];
1081
+ if (member !== this) {
1082
+ collectHandlers(member, this.peers.name);
1083
+ }
1084
+ }
1085
+ }
1086
+
1087
+ // Collect from usesTeams (teams this agent uses as a client)
1088
+ for (const team of this.usesTeams) {
1089
+ if (team && team.members) {
1090
+ const memberNames = Object.keys(team.members);
1091
+ for (const memberName of memberNames) {
1092
+ const member = team.members[memberName];
1093
+ collectHandlers(member, team.name);
1094
+ }
1095
+ }
1096
+ }
1097
+
1098
+ if (capabilities.length === 0) {
1099
+ return '';
1100
+ }
1101
+
1102
+ let doc = '\nAvailable team member capabilities:\n';
1103
+ for (const cap of capabilities) {
1104
+ doc += `\n${cap.agent} [${cap.role}]:\n`;
1105
+ for (const handler of cap.handlers) {
1106
+ doc += ` - ${handler.name}: ${handler.description}\n`;
1107
+ }
1108
+ }
1109
+ doc += '\nTo delegate, use: { "intent": "handler_name", "data": {...} }\n';
1110
+
1111
+ return doc;
1112
+ }
1113
+
1114
+ /**
1115
+ * Generate peer capabilities formatted as available actions
1116
+ * Returns a string listing delegation actions in the same format as action registry
1117
+ */
1118
+ getPeerCapabilitiesAsActions() {
1119
+ const capabilities = [];
1120
+ const processedAgents = new Set();
1121
+
1122
+ // Helper function to collect handlers from an agent
1123
+ const collectHandlers = (agent, teamName = null) => {
1124
+ if (!agent || processedAgents.has(agent.name)) {
1125
+ return;
1126
+ }
1127
+ processedAgents.add(agent.name);
1128
+
1129
+ if (agent.handlers && Object.keys(agent.handlers).length > 0) {
1130
+ const handlers = Object.keys(agent.handlers);
1131
+ for (const handler of handlers) {
1132
+ const agentInfo = teamName ? `${agent.name} (${teamName})` : agent.name;
1133
+
1134
+ // Extract affordance/description from handler
1135
+ let description = '';
1136
+ const handlerFn = agent.handlers[handler];
1137
+
1138
+ if (handlerFn && handlerFn.__playbook__) {
1139
+ // Extract first line or first sentence from playbook as description
1140
+ const playbook = handlerFn.__playbook__;
1141
+ const lines = playbook.split('\n');
1142
+ const firstLine = lines[0].trim();
1143
+
1144
+ // Remove template variables for cleaner description
1145
+ description = firstLine.replace(/\$\{[^}]+\}/g, '...').substring(0, 80);
1146
+ if (description.length < firstLine.length) {
1147
+ description += '...';
1148
+ }
1149
+
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
+ }
1165
+ }
1166
+ } else if (handlerFn && typeof handlerFn === 'function') {
1167
+ // For regular functions, generate description from name
1168
+ description = `Handle ${handler} event`;
1169
+ }
1170
+
1171
+ capabilities.push({
1172
+ intent: handler,
1173
+ agent: agentInfo,
1174
+ role: agent.role ? agent.role.name : 'Unknown',
1175
+ description: description || `Execute ${handler}`
1176
+ });
1177
+ }
1178
+ }
1179
+ };
1180
+
1181
+ // Collect from peers team (if this agent is a member of a team)
1182
+ if (this.peers && this.peers.members) {
1183
+ const memberNames = Object.keys(this.peers.members);
1184
+ for (const memberName of memberNames) {
1185
+ const member = this.peers.members[memberName];
1186
+ if (member !== this) {
1187
+ collectHandlers(member, this.peers.name);
1188
+ }
1189
+ }
1190
+ }
1191
+
1192
+ // Collect from usesTeams (teams this agent uses as a client)
1193
+ for (const team of this.usesTeams) {
1194
+ if (team && team.members) {
1195
+ const memberNames = Object.keys(team.members);
1196
+ for (const memberName of memberNames) {
1197
+ const member = team.members[memberName];
1198
+ collectHandlers(member, team.name);
1199
+ }
1200
+ }
1201
+ }
1202
+
1203
+ if (capabilities.length === 0) {
1204
+ return '';
1205
+ }
1206
+
1207
+ let doc = '\n\nDelegation actions (to team members):\n';
1208
+ for (const cap of capabilities) {
1209
+ doc += `- { "actionType": "delegate", "intent": "${cap.intent}", "data": ... } - ${cap.description} (Delegate to ${cap.agent} [${cap.role}])\n`;
1210
+ }
1211
+
1212
+ return doc;
1213
+ }
1214
+
1215
+ /**
1216
+ * Check if action can be executed directly with LLM
1217
+ */
1218
+ canExecuteDirectly(action) {
1219
+ // Has inline playbook
1220
+ if (action.playbook) return true;
1221
+
1222
+ // Explicit LLM task
1223
+ if (action.type === 'llm_task') return true;
1224
+
1225
+ // Simple state operations
1226
+ if (action.type === 'update_state' || action.type === 'return') return true;
1227
+
1228
+ // If it's a very simple task description, LLM can handle it
1229
+ const intent = action.intent || action.description || '';
1230
+ if (intent.length < 100 && !action.requiresExternalAgent) {
1231
+ return true;
1232
+ }
1233
+
1234
+ return false;
1235
+ }
1236
+
1237
+ /**
1238
+ * Execute action directly with LLM
1239
+ */
1240
+ async executeDirectly(action, context) {
1241
+ // Check if this is a registered action with an executor
1242
+ const actionDef = actionRegistry.get(action.type);
1243
+
1244
+ if (actionDef && actionDef.execute) {
1245
+ // Use the registered executor
1246
+ return await actionDef.execute(action, this);
1247
+ }
1248
+
1249
+ // Execute with LLM
1250
+ if (!this.llmProvider) {
1251
+ this.llmProvider = new LLMProvider(this.llm);
1252
+ }
1253
+
1254
+ let prompt;
1255
+
1256
+ if (action.playbook) {
1257
+ prompt = action.playbook;
1258
+ } else {
1259
+ // Generate simple prompt
1260
+ const intent = action.intent || action.description;
1261
+ const data = action.data || action.input || {};
1262
+
1263
+ prompt = `
1264
+ Task: ${intent}
1265
+
1266
+ Input data:
1267
+ ${JSON.stringify(data, null, 2)}
1268
+
1269
+ Context:
1270
+ ${JSON.stringify(context, null, 2)}
1271
+
1272
+ Execute this task and return the result as JSON.
1273
+ `;
1274
+ }
1275
+
1276
+ return await this.llmProvider.executePlaybook(prompt, context, this.name, [], this, false);
1277
+ }
1278
+
1279
+ /**
1280
+ * Find a skill that matches the given intent
1281
+ */
1282
+ findMatchingSkill(intent) {
1283
+ if (!this.skills || this.skills.length === 0) {
1284
+ return null;
1285
+ }
1286
+
1287
+ if (!intent || typeof intent !== 'string') {
1288
+ return null;
1289
+ }
1290
+
1291
+ const intentLower = intent.toLowerCase();
1292
+
1293
+ // Try exact or partial match
1294
+ for (const skill of this.skills) {
1295
+ const skillLower = skill.toLowerCase();
1296
+
1297
+ if (intentLower.includes(skillLower) || skillLower.includes(intentLower)) {
1298
+ return skill;
1299
+ }
1300
+ }
1301
+
1302
+ // Try keyword matching
1303
+ const keywords = intentLower.split(/\s+/);
1304
+
1305
+ for (const skill of this.skills) {
1306
+ const skillLower = skill.toLowerCase();
1307
+
1308
+ for (const keyword of keywords) {
1309
+ if (keyword.length > 3 && skillLower.includes(keyword)) {
1310
+ return skill;
1311
+ }
1312
+ }
1313
+ }
1314
+
1315
+ return null;
1316
+ }
1317
+
1318
+ /**
1319
+ * Execute legacy action (fallback for actions without executors)
1320
+ * This should rarely be used now - most actions have executors
1321
+ */
1322
+ async executeLegacyAction(action) {
1323
+ throw new Error(`Action type "${action.type}" has no executor registered and no legacy handler`);
1324
+ }
1325
+
1326
+
1327
+ async callSkill(skillName, input) {
1328
+ // Calling skill
1329
+
1330
+ if (!this.skills.includes(skillName)) {
1331
+ throw new Error(`Agent ${this.name} does not have skill: ${skillName}`);
1332
+ }
1333
+
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 };
1338
+ }
1339
+
1340
+ /**
1341
+ * Get available skill functions for tool calling
1342
+ * Returns an array of { name, fn, description } for each available function
1343
+ */
1344
+ getSkillFunctions() {
1345
+ const functions = [];
1346
+
1347
+ // Access SkillRegistry from global scope (set by transpiled code)
1348
+ if (typeof globalThis.SkillRegistry !== 'undefined') {
1349
+ for (const skillName of this.skills) {
1350
+ const skillFunctions = globalThis.SkillRegistry.getAll(skillName);
1351
+ for (const [funcName, { fn, metadata }] of Object.entries(skillFunctions)) {
1352
+ functions.push({
1353
+ name: funcName,
1354
+ fn,
1355
+ description: metadata.affordance || `Function from ${skillName} skill`
1356
+ });
1357
+ }
1358
+ }
1359
+ }
1360
+
1361
+ return functions;
1362
+ }
1363
+
1364
+
1365
+ toString() {
1366
+ return `Agent(${this.name}:${this.role.name})`;
1367
+ }
1368
+ }