node-red-contrib-ai-agent 0.5.11 → 0.5.12

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.
@@ -76,32 +76,32 @@ module.exports = function (RED) {
76
76
 
77
77
  // 3. Execution Phase (Direct Call)
78
78
  msg.orchestration.currentTaskId = nextTask.id;
79
- const agentInfo = msg.orchestration.availableAgents.find(a =>
80
- a.capabilities.some(cap => cap.toLowerCase() === nextTask.type.toLowerCase())
81
- );
79
+ const agentInfo = selectAgentForTask(msg.orchestration, nextTask);
82
80
 
83
81
  if (!agentInfo) {
84
82
  node.warn(`No registered agent found for capability: ${nextTask.type}`);
85
- throw new Error(`Capability not provided by any wired agent: ${nextTask.type}`);
86
- }
87
-
88
- const agentNode = RED.nodes.getNode(agentInfo.id);
89
- if (!agentNode || typeof agentNode.executeTask !== 'function') {
90
- throw new Error(`Agent node ${agentInfo.name} [${agentInfo.id}] is not an AI Orchestrator Agent or is missing executeTask API.`);
91
- }
92
-
93
- node.status({ fill: 'blue', shape: 'ring', text: `agent: ${agentInfo.name}` });
94
- try {
95
- const result = await agentNode.executeTask(nextTask.input, msg);
96
- msg.payload = result;
97
- msg.error = null;
98
- } catch (err) {
99
- // Strip 'AI API Error: ' prefix if present to match test expectations
100
- let errorMessage = err.message;
101
- if (errorMessage.startsWith('AI API Error: ')) {
102
- errorMessage = errorMessage.substring('AI API Error: '.length);
83
+ msg.error = `Capability not provided by any wired agent: ${nextTask.type}`;
84
+ msg.payload = null;
85
+ } else {
86
+ const agentNode = RED.nodes.getNode(agentInfo.id);
87
+ if (!agentNode || typeof agentNode.executeTask !== 'function') {
88
+ msg.error = `Agent node ${agentInfo.name} [${agentInfo.id}] is not an AI Orchestrator Agent or is missing executeTask API.`;
89
+ msg.payload = null;
90
+ } else {
91
+ node.status({ fill: 'blue', shape: 'ring', text: `agent: ${agentInfo.name}` });
92
+ try {
93
+ const result = await agentNode.executeTask(nextTask.input, msg);
94
+ msg.payload = result;
95
+ msg.error = null;
96
+ } catch (err) {
97
+ // Strip 'AI API Error: ' prefix if present to match test expectations
98
+ let errorMessage = err.message;
99
+ if (errorMessage.startsWith('AI API Error: ')) {
100
+ errorMessage = errorMessage.substring('AI API Error: '.length);
101
+ }
102
+ msg.error = errorMessage;
103
+ }
103
104
  }
104
- msg.error = errorMessage;
105
105
  }
106
106
 
107
107
  // 4. Reflection Phase
@@ -133,6 +133,10 @@ module.exports = function (RED) {
133
133
  const strategy = node.planningStrategy;
134
134
  const agents = msg.orchestration.availableAgents || [];
135
135
  const agentManifest = agents.map(a => `- ${a.name}: [${a.capabilities.join(', ')}]`).join('\n');
136
+ const allowedCapabilities = Array.from(new Set(
137
+ agents.flatMap(a => Array.isArray(a.capabilities) ? a.capabilities : []).map(String)
138
+ ));
139
+ const allowedCapabilitiesJson = JSON.stringify(allowedCapabilities);
136
140
 
137
141
  let prompt = `Goal: ${goal}\n\nAvailable Agents and their Capabilities:\n${agentManifest}\n\nDecompose this goal into a series of tasks. You MUST ONLY use capabilities provided by the available agents listed above.
138
142
  Return a JSON object with a "tasks" array. Each task should have:
@@ -154,6 +158,11 @@ Return a JSON object with a "tasks" array. Each task should have:
154
158
  - Do NOT include trailing commas
155
159
  - All string values must be valid JSON strings (escape newlines as \\n if needed)
156
160
 
161
+ CAPABILITY RULES:
162
+ - The "type" field MUST be one of these EXACT strings (case-sensitive): ${allowedCapabilitiesJson}
163
+ - Do NOT invent new capabilities.
164
+ - If the goal seems to require a missing capability, still produce a plan using ONLY the allowed capabilities, and include a task whose input explains the limitation.
165
+
157
166
  Example:
158
167
  {
159
168
  "tasks": [
@@ -181,8 +190,8 @@ Example:
181
190
  */
182
191
  async function reflectAndRefine(node, msg) {
183
192
  const currentTaskId = msg.orchestration.currentTaskId;
193
+ const isError = !!msg.error;
184
194
  const taskResult = msg.payload;
185
- const isError = msg.error ? true : false;
186
195
 
187
196
  // Update history
188
197
  msg.orchestration.history.push({
@@ -204,17 +213,38 @@ Example:
204
213
  }
205
214
  }
206
215
 
216
+ const hasHumanApprovalCapability = (msg.orchestration.availableAgents || []).some(a =>
217
+ Array.isArray(a.capabilities) && a.capabilities.some(c => String(c).toLowerCase() === 'human_approval')
218
+ );
219
+
220
+ const allowedCapabilities = Array.from(new Set(
221
+ (msg.orchestration.availableAgents || [])
222
+ .flatMap(a => Array.isArray(a.capabilities) ? a.capabilities : [])
223
+ .map(String)
224
+ ));
225
+ const allowedCapabilitiesJson = JSON.stringify(allowedCapabilities);
226
+
227
+ const humanApprovalInstruction = hasHumanApprovalCapability
228
+ ? '3. If you need more information or approval from a human, add a task with type "human_approval".'
229
+ : '3. Do NOT request human approval tasks ("human_approval" is not available).';
230
+
207
231
  const prompt = `Current Goal: ${msg.orchestration.goal}
208
232
  Current Plan: ${JSON.stringify(msg.orchestration.plan)}
209
233
  Last Task ID: ${currentTaskId}
210
234
  Last Task ${isError ? 'Error' : 'Result'}: ${JSON.stringify(isError ? msg.error : taskResult)}
211
235
 
236
+ Available Capabilities (EXACT strings): ${allowedCapabilitiesJson}
237
+
212
238
  Evaluate the progress.
213
239
  1. If the last task failed, propose a recovery strategy (retry, alternative task, or fail the goal).
214
240
  2. If the goal is achieved, set status to "completed".
215
- 3. If you need more information or approval from a human, add a task with type "human_approval".
241
+ ${humanApprovalInstruction}
216
242
  4. Otherwise, continue execution. You may refine the plan by adding, removing, or modifying tasks.
217
243
 
244
+ PLAN UPDATE RULES:
245
+ - In updatedPlan.tasks, every task.type MUST be one of the EXACT capability strings listed above.
246
+ - Do NOT invent or rename capabilities.
247
+
218
248
  Return a JSON object:
219
249
  {
220
250
  "analysis": "detailed evaluation of progress and next steps",
@@ -269,6 +299,35 @@ Return a JSON object:
269
299
  return eligibleTasks[0];
270
300
  }
271
301
 
302
+ function selectAgentForTask(orchestration, task) {
303
+ if (!orchestration || !task || !task.type) return null;
304
+ const availableAgents = orchestration.availableAgents || [];
305
+ const taskType = String(task.type).toLowerCase();
306
+
307
+ const matchingAgents = availableAgents.filter(a =>
308
+ Array.isArray(a.capabilities) && a.capabilities.some(cap => String(cap).toLowerCase() === taskType)
309
+ );
310
+
311
+ if (matchingAgents.length === 0) return null;
312
+ if (matchingAgents.length === 1) return matchingAgents[0];
313
+
314
+ orchestration.agentUsage = orchestration.agentUsage || {};
315
+ orchestration.agentLastUsedAt = orchestration.agentLastUsedAt || {};
316
+
317
+ // Prefer least recently used among matching agents to avoid always picking the first.
318
+ matchingAgents.sort((a, b) => {
319
+ const aLast = orchestration.agentLastUsedAt[a.id] || 0;
320
+ const bLast = orchestration.agentLastUsedAt[b.id] || 0;
321
+ if (aLast !== bLast) return aLast - bLast;
322
+ return String(a.id).localeCompare(String(b.id));
323
+ });
324
+
325
+ const selected = matchingAgents[0];
326
+ orchestration.agentUsage[selected.id] = (orchestration.agentUsage[selected.id] || 0) + 1;
327
+ orchestration.agentLastUsedAt[selected.id] = Date.now();
328
+ return selected;
329
+ }
330
+
272
331
  /**
273
332
  * Makes an API call to the AI model
274
333
  * @param {Object} aiConfig - AI configuration containing model and API key
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-red-contrib-ai-agent",
3
- "version": "0.5.11",
3
+ "version": "0.5.12",
4
4
  "description": "AI Agent for Node-RED",
5
5
  "repository": {
6
6
  "type": "git",