node-red-contrib-ai-agent 0.5.15 → 0.5.17

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.
@@ -13,8 +13,8 @@
13
13
  debug: { value: false }
14
14
  },
15
15
  inputs: 1,
16
- outputs: 1,
17
- outputLabels: ["Final Result"],
16
+ outputs: 2,
17
+ outputLabels: ["Success", "Failure"],
18
18
  icon: "font-awesome/fa-sitemap",
19
19
  label: function () {
20
20
  return this.name || "AI Orchestrator";
@@ -76,16 +76,20 @@
76
76
 
77
77
  <h3>Outputs</h3>
78
78
  <ol class="node-ports">
79
- <li>Task Dispatch
79
+ <li>Success
80
80
  <dl class="message-properties">
81
- <dt>msg.payload <span class="property-type">string</span></dt>
82
- <dd>The instruction for the next task.</dd>
81
+ <dt>msg.payload <span class="property-type">string | object</span></dt>
82
+ <dd>The final successful result once the plan completes.</dd>
83
+ <dt>msg.orchestration <span class="property-type">object</span></dt>
84
+ <dd>Complete orchestration metadata and history.</dd>
83
85
  </dl>
84
86
  </li>
85
- <li>Final Result
87
+ <li>Failure
86
88
  <dl class="message-properties">
89
+ <dt>msg.error <span class="property-type">string</span></dt>
90
+ <dd>Error message explaining why orchestration failed.</dd>
87
91
  <dt>msg.orchestration <span class="property-type">object</span></dt>
88
- <dd>The full orchestration history and final result status.</dd>
92
+ <dd>Orchestration state including failure details.</dd>
89
93
  </dl>
90
94
  </li>
91
95
  </ol>
@@ -98,5 +102,5 @@
98
102
  <li><strong>Priorities:</strong> Tasks with higher <code>priority</code> numbers are executed first.</li>
99
103
  <li><strong>Error Recovery:</strong> If a task fails, the orchestrator reflects on the error and can revise the plan to retry or try an alternative approach.</li>
100
104
  </ul>
101
- <p>When the goal is achieved or max iterations are reached, the final message is sent to the second output.</p>
105
+ <p>The first output fires when the orchestration succeeds. All failure paths (validation errors, missing agents, iteration limits, plan errors, etc.) send a message out of the second output with <code>msg.error</code> describing the issue.</p>
102
106
  </script>
@@ -64,9 +64,11 @@ module.exports = function (RED) {
64
64
  msg.orchestration._running = true;
65
65
  setImmediate(() => processNextStep(RED, node, msg, send, done));
66
66
  } catch (error) {
67
- node.status({ fill: 'red', shape: 'ring', text: 'error' });
68
67
  node.error(error.message, msg);
69
- if (done) done(error);
68
+ msg.orchestration = msg.orchestration || {};
69
+ msg.orchestration.status = 'failed';
70
+ msg.orchestration.error = error && error.message ? error.message : String(error);
71
+ finalizeOrchestration(node, msg, send, done, error);
70
72
  }
71
73
  });
72
74
  }
@@ -155,9 +157,25 @@ module.exports = function (RED) {
155
157
  }
156
158
 
157
159
  function finalizeOrchestration(node, msg, send, done, error) {
160
+ send = send || function () { node.send.apply(node, arguments) };
161
+ msg.orchestration = msg.orchestration || {};
158
162
  msg.orchestration._running = false;
159
- node.status({ fill: msg.orchestration.status === 'completed' ? 'green' : 'red', shape: 'dot', text: msg.orchestration.status });
160
- send(msg);
163
+
164
+ const isSuccess = msg.orchestration.status === 'completed';
165
+ if (!isSuccess) {
166
+ const errorMessage = msg.orchestration.error || (error && error.message) || msg.error || 'Unknown error';
167
+ msg.error = errorMessage;
168
+ msg.orchestration.error = errorMessage;
169
+ }
170
+
171
+ node.status({
172
+ fill: isSuccess ? 'green' : 'red',
173
+ shape: 'dot',
174
+ text: msg.orchestration.status || (isSuccess ? 'completed' : 'failed')
175
+ });
176
+
177
+ const outputs = isSuccess ? [msg, null] : [null, msg];
178
+ send(outputs);
161
179
  if (done) done(error);
162
180
  }
163
181
 
@@ -216,6 +234,14 @@ Example:
216
234
  const response = await callAI(node, msg.aiagent, prompt, "You are an AI Orchestrator that creates non-linear plans with dependencies.");
217
235
  debugLog(node, 'Planning Response', response);
218
236
  const planData = parseJsonResponse(response);
237
+
238
+ if (!planData || typeof planData !== 'object') {
239
+ throw new Error('Planning response was not valid JSON.');
240
+ }
241
+ if (!Array.isArray(planData.tasks)) {
242
+ throw new Error('Planning response must include a tasks array.');
243
+ }
244
+
219
245
  msg.orchestration.plan = planData;
220
246
  msg.orchestration.status = 'executing';
221
247
  } catch (error) {
@@ -303,6 +329,10 @@ Return a JSON object:
303
329
  const response = await callAI(node, msg.aiagent, prompt, "You are an AI Orchestrator that reflects on progress and manages plan revisions.");
304
330
  const reflection = parseJsonResponse(response);
305
331
 
332
+ if (!reflection || typeof reflection !== 'object') {
333
+ throw new Error('Reflection response was not valid JSON.');
334
+ }
335
+
306
336
  msg.orchestration.status = reflection.status;
307
337
  if (reflection.updatedPlan) {
308
338
  msg.orchestration.plan = reflection.updatedPlan;
@@ -425,23 +455,89 @@ Return a JSON object:
425
455
  * @returns {string} The extracted JSON string
426
456
  */
427
457
  function extractJson(text) {
458
+ if (typeof text !== 'string') return '';
459
+
460
+ // Prefer a balanced-brace extraction to avoid greedy matching issues.
461
+ const extracted = extractBalancedJsonObject(text);
462
+ if (extracted) return extracted;
463
+
464
+ // Fallback to a simple greedy match if needed.
428
465
  const match = text.match(/\{[\s\S]*\}/);
429
466
  return match ? match[0] : text;
430
467
  }
431
468
 
469
+ /**
470
+ * Extract the first balanced JSON object (starting at the first '{') from arbitrary text.
471
+ * @param {string} text - The input text
472
+ * @returns {string} Extracted JSON object text or empty string if not found
473
+ */
474
+ function extractBalancedJsonObject(text) {
475
+ const start = text.indexOf('{');
476
+ if (start === -1) return '';
477
+
478
+ let depth = 0;
479
+ let inString = false;
480
+ let escape = false;
481
+
482
+ for (let i = start; i < text.length; i++) {
483
+ const ch = text[i];
484
+
485
+ if (inString) {
486
+ if (escape) {
487
+ escape = false;
488
+ continue;
489
+ }
490
+ if (ch === '\\') {
491
+ escape = true;
492
+ continue;
493
+ }
494
+ if (ch === '"') {
495
+ inString = false;
496
+ }
497
+ continue;
498
+ }
499
+
500
+ if (ch === '"') {
501
+ inString = true;
502
+ escape = false;
503
+ continue;
504
+ }
505
+
506
+ if (ch === '{') {
507
+ depth++;
508
+ continue;
509
+ }
510
+
511
+ if (ch === '}') {
512
+ depth--;
513
+ if (depth === 0) {
514
+ return text.slice(start, i + 1);
515
+ }
516
+ }
517
+ }
518
+
519
+ return '';
520
+ }
521
+
432
522
  /**
433
523
  * Parses a JSON object from an AI response, tolerating common non-JSON wrappers.
524
+ * Returns an empty string if parsing fails after sanitization attempts.
434
525
  * @param {string} text - The AI response
435
- * @returns {any} Parsed JSON
526
+ * @returns {any|string} Parsed JSON or empty string on failure
436
527
  */
437
528
  function parseJsonResponse(text) {
438
529
  const extracted = extractJson(text);
439
530
  try {
440
531
  return JSON.parse(extracted);
441
- } catch (_err) {
532
+ } catch (err1) {
442
533
  const sanitized = sanitizeJsonLikeText(extracted);
443
- return JSON.parse(sanitized);
534
+ try {
535
+ return JSON.parse(sanitized);
536
+ } catch (err2) {
537
+ // no need to catch anything, default to empty string
538
+ }
444
539
  }
540
+ return "";
445
541
  }
446
542
 
447
543
  /**
@@ -528,7 +624,62 @@ Return a JSON object:
528
624
  out += ch;
529
625
  }
530
626
 
531
- return out.trim();
627
+ return removeTrailingCommas(out).trim();
628
+ }
629
+
630
+ /**
631
+ * Removes trailing commas before closing braces/brackets outside of string literals.
632
+ * Example: {"a":1,} -> {"a":1}
633
+ * @param {string} input - JSON-like string
634
+ * @returns {string}
635
+ */
636
+ function removeTrailingCommas(input) {
637
+ if (typeof input !== 'string') return '';
638
+
639
+ let out = '';
640
+ let inString = false;
641
+ let escape = false;
642
+
643
+ for (let i = 0; i < input.length; i++) {
644
+ const ch = input[i];
645
+
646
+ if (inString) {
647
+ out += ch;
648
+ if (escape) {
649
+ escape = false;
650
+ continue;
651
+ }
652
+ if (ch === '\\') {
653
+ escape = true;
654
+ continue;
655
+ }
656
+ if (ch === '"') {
657
+ inString = false;
658
+ }
659
+ continue;
660
+ }
661
+
662
+ if (ch === '"') {
663
+ out += ch;
664
+ inString = true;
665
+ escape = false;
666
+ continue;
667
+ }
668
+
669
+ if (ch === ',') {
670
+ // Look ahead for the next non-whitespace character.
671
+ let j = i + 1;
672
+ while (j < input.length && /\s/.test(input[j])) j++;
673
+ const nextNonWs = j < input.length ? input[j] : '';
674
+ if (nextNonWs === '}' || nextNonWs === ']') {
675
+ continue;
676
+ }
677
+ }
678
+
679
+ out += ch;
680
+ }
681
+
682
+ return out;
532
683
  }
533
684
 
534
685
  function debugLog(node, label, payload) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-red-contrib-ai-agent",
3
- "version": "0.5.15",
3
+ "version": "0.5.17",
4
4
  "description": "AI Agent for Node-RED",
5
5
  "repository": {
6
6
  "type": "git",