@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.
- package/QUICKSTART.md +89 -0
- package/README.md +545 -0
- package/examples/actions-demo.koi +177 -0
- package/examples/cache-test.koi +29 -0
- package/examples/calculator.koi +61 -0
- package/examples/clear-registry.js +33 -0
- package/examples/clear-registry.koi +30 -0
- package/examples/code-introspection-test.koi +149 -0
- package/examples/counter.koi +132 -0
- package/examples/delegation-test.koi +52 -0
- package/examples/directory-import-test.koi +84 -0
- package/examples/hello-world-claude.koi +52 -0
- package/examples/hello-world.koi +52 -0
- package/examples/hello.koi +24 -0
- package/examples/mcp-example.koi +70 -0
- package/examples/multi-event-handler-test.koi +144 -0
- package/examples/new-import-test.koi +89 -0
- package/examples/pipeline.koi +162 -0
- package/examples/registry-demo.koi +184 -0
- package/examples/registry-playbook-demo.koi +162 -0
- package/examples/registry-playbook-email-compositor-2.koi +140 -0
- package/examples/registry-playbook-email-compositor.koi +140 -0
- package/examples/sentiment.koi +90 -0
- package/examples/simple.koi +48 -0
- package/examples/skill-import-test.koi +76 -0
- package/examples/skills/advanced/index.koi +95 -0
- package/examples/skills/math-operations.koi +69 -0
- package/examples/skills/string-operations.koi +56 -0
- package/examples/task-chaining-demo.koi +244 -0
- package/examples/test-await.koi +22 -0
- package/examples/test-crypto-sha256.koi +196 -0
- package/examples/test-delegation.koi +41 -0
- package/examples/test-multi-team-routing.koi +258 -0
- package/examples/test-no-handler.koi +35 -0
- package/examples/test-npm-import.koi +67 -0
- package/examples/test-parse.koi +10 -0
- package/examples/test-peers-with-team.koi +59 -0
- package/examples/test-permissions-fail.koi +20 -0
- package/examples/test-permissions.koi +36 -0
- package/examples/test-simple-registry.koi +31 -0
- package/examples/test-typescript-import.koi +64 -0
- package/examples/test-uses-team-syntax.koi +25 -0
- package/examples/test-uses-team.koi +31 -0
- package/examples/utils/calculator.test.ts +144 -0
- package/examples/utils/calculator.ts +56 -0
- package/examples/utils/math-helpers.js +50 -0
- package/examples/utils/math-helpers.ts +55 -0
- package/examples/web-delegation-demo.koi +165 -0
- package/package.json +78 -0
- package/src/cli/koi.js +793 -0
- package/src/compiler/build-optimizer.js +447 -0
- package/src/compiler/cache-manager.js +274 -0
- package/src/compiler/import-resolver.js +369 -0
- package/src/compiler/parser.js +7542 -0
- package/src/compiler/transpiler.js +1105 -0
- package/src/compiler/typescript-transpiler.js +148 -0
- package/src/grammar/koi.pegjs +767 -0
- package/src/runtime/action-registry.js +172 -0
- package/src/runtime/actions/call-skill.js +45 -0
- package/src/runtime/actions/format.js +115 -0
- package/src/runtime/actions/print.js +42 -0
- package/src/runtime/actions/registry-delete.js +37 -0
- package/src/runtime/actions/registry-get.js +37 -0
- package/src/runtime/actions/registry-keys.js +33 -0
- package/src/runtime/actions/registry-search.js +34 -0
- package/src/runtime/actions/registry-set.js +50 -0
- package/src/runtime/actions/return.js +31 -0
- package/src/runtime/actions/send-message.js +58 -0
- package/src/runtime/actions/update-state.js +36 -0
- package/src/runtime/agent.js +1368 -0
- package/src/runtime/cli-logger.js +205 -0
- package/src/runtime/incremental-json-parser.js +201 -0
- package/src/runtime/index.js +33 -0
- package/src/runtime/llm-provider.js +1372 -0
- package/src/runtime/mcp-client.js +1171 -0
- package/src/runtime/planner.js +273 -0
- package/src/runtime/registry-backends/keyv-sqlite.js +215 -0
- package/src/runtime/registry-backends/local.js +260 -0
- package/src/runtime/registry.js +162 -0
- package/src/runtime/role.js +14 -0
- package/src/runtime/router.js +395 -0
- package/src/runtime/runtime.js +113 -0
- package/src/runtime/skill-selector.js +173 -0
- package/src/runtime/skill.js +25 -0
- 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
|
+
}
|