@tbrandenburg/node-red-agents 0.2.0 → 0.3.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.
@@ -84,6 +84,9 @@
84
84
  runtime: { value: 'direct' },
85
85
  invocation: { value: 'prompt' },
86
86
 
87
+ agentName: { value: '' },
88
+ agentNameType: { value: 'str' },
89
+
87
90
  model: { value: '' },
88
91
  modelType: { value: 'str' },
89
92
 
@@ -140,6 +143,11 @@
140
143
  MODEL_SUGGESTIONS.forEach((m) => datalist.append('<option value="' + m + '">'));
141
144
  }
142
145
 
146
+ $('#node-input-agentName').typedInput({
147
+ typeField: '#node-input-agentNameType',
148
+ types: ['str', 'msg', 'flow', 'global', 'env']
149
+ });
150
+
143
151
  $('#node-input-prompt').typedInput({
144
152
  typeField: '#node-input-promptType',
145
153
  types: ['msg', 'str', 'flow', 'global', 'env']
@@ -317,6 +325,15 @@
317
325
  <input type="text" id="node-input-name" placeholder="Agent">
318
326
  </div>
319
327
 
328
+ <div class="form-row">
329
+ <label for="node-input-agentName"><i class="fa fa-id-badge"></i> Agent name
330
+ <i class="fa fa-info-circle" style="color:#888; cursor:help; margin-left:4px;"
331
+ title="Reported as msg.agentName on the result/event outputs. Leave blank (str) to use the Name field above; set to msg/flow/global to make it dynamic per message."></i>
332
+ </label>
333
+ <input type="text" id="node-input-agentName" placeholder="leave blank to use the Name field above" style="width:60%">
334
+ <input type="hidden" id="node-input-agentNameType">
335
+ </div>
336
+
320
337
  <div class="form-row">
321
338
  <label for="node-input-agent"><i class="fa fa-cube"></i> Agent</label>
322
339
  <select id="node-input-agent" style="width:70%">
@@ -494,6 +511,16 @@
494
511
  conversation on the next message.</dd>
495
512
  <dt>agentExecution <span class="property-type">object</span></dt>
496
513
  <dd>id, status, exitCode, signal, timedOut, durationMs, sessionID.</dd>
514
+ <dt>agentId <span class="property-type">string</span></dt>
515
+ <dd>This node instance's own Node-RED id, so a flow with
516
+ several agent nodes can tell which one produced a
517
+ given message.</dd>
518
+ <dt>agentName <span class="property-type">string</span></dt>
519
+ <dd>The resolved Agent name field, falling back to this node
520
+ instance's configured Name if the Agent name field is
521
+ blank/unresolved for this message (e.g. via
522
+ <code>msg.agentName</code> when configured as a msg
523
+ property).</dd>
497
524
  </dl>
498
525
  </li>
499
526
  <li>Events
@@ -504,6 +531,10 @@
504
531
  <dt>executionId <span class="property-type">string</span></dt>
505
532
  <dd>Correlates events (and the eventual result) belonging to
506
533
  the same execution when several run concurrently.</dd>
534
+ <dt>agentId <span class="property-type">string</span></dt>
535
+ <dd>This node instance's own Node-RED id (see Result output).</dd>
536
+ <dt>agentName <span class="property-type">string</span></dt>
537
+ <dd>The resolved Agent name for this execution (see Result output).</dd>
507
538
  </dl>
508
539
  </li>
509
540
  </ol>
@@ -45,6 +45,14 @@ module.exports = function (RED) {
45
45
  node.runtime = config.runtime || "direct";
46
46
  node.invocation = config.invocation || "prompt";
47
47
 
48
+ // Typed-input override of the agentName reported on outgoing/event
49
+ // messages (see lifecycleEnvelope below). Same typed-input pattern as
50
+ // every other overridable field: blank/unresolved falls back to
51
+ // node.name, so existing flows relying on the (previously static)
52
+ // node.name value keep working unchanged.
53
+ node.agentName = config.agentName !== undefined ? config.agentName : "";
54
+ node.agentNameType = config.agentNameType || "str";
55
+
48
56
  node.model = config.model || "";
49
57
  node.modelType = config.modelType || "str";
50
58
 
@@ -150,19 +158,30 @@ module.exports = function (RED) {
150
158
  }
151
159
  }
152
160
 
161
+ // Per-message resolution of the reported agentName: the typed-input
162
+ // agentName/agentNameType field (e.g. msg.agentName) if configured and
163
+ // present, else node.name -- same fallback behavior a blank/unset
164
+ // typed-input field has always had for every other field.
165
+ function resolveAgentName(msg) {
166
+ const v = resolveTyped(node.agentName, node.agentNameType, msg, node.name);
167
+ return v === undefined || v === null || v === "" ? node.name : String(v).trim();
168
+ }
169
+
153
170
  // Common envelope for every message on output 2 (the lifecycle/event
154
171
  // stream): correlation (topic, executionId), live scheduler counts
155
172
  // (so a widget bound to this stream always has the current
156
173
  // active/queued numbers, no separate polling needed) and a
157
174
  // timestamp (so external aggregation/history doesn't have to rely
158
175
  // on message-arrival time).
159
- function lifecycleEnvelope(msg, executionId, payload) {
176
+ function lifecycleEnvelope(msg, executionId, payload, agentName) {
160
177
  return {
161
178
  _msgid: msg._msgid,
162
179
  topic: msg.topic,
163
180
  payload,
164
181
  agent: node.agent,
165
182
  runtime: node.runtime,
183
+ agentId: node.id,
184
+ agentName,
166
185
  executionId,
167
186
  active: node.scheduler.activeCount,
168
187
  queued: node.scheduler.queuedCount,
@@ -170,8 +189,8 @@ module.exports = function (RED) {
170
189
  };
171
190
  }
172
191
 
173
- function emitEvent(send, msg, executionId, type) {
174
- send([null, lifecycleEnvelope(msg, executionId, { type })]);
192
+ function emitEvent(send, msg, executionId, type, agentName) {
193
+ send([null, lifecycleEnvelope(msg, executionId, { type }, agentName)]);
175
194
  }
176
195
 
177
196
  // The actual work for one execution. Only ever invoked by the
@@ -188,11 +207,11 @@ module.exports = function (RED) {
188
207
  resolved,
189
208
  executionId,
190
209
  onEvent: (event) => {
191
- send([null, lifecycleEnvelope(msg, executionId, event)]);
210
+ send([null, lifecycleEnvelope(msg, executionId, event, resolved.agentName)]);
192
211
  },
193
212
  onStatus: (status) => {
194
213
  if (status === "running") {
195
- emitEvent(send, msg, executionId, "running");
214
+ emitEvent(send, msg, executionId, "running", resolved.agentName);
196
215
  } else {
197
216
  // Terminal (completed/failed/timeout): stash rather
198
217
  // than emit immediately -- the scheduler hasn't
@@ -214,6 +233,8 @@ module.exports = function (RED) {
214
233
  payload: result.payload,
215
234
  agent: node.agent,
216
235
  runtime: node.runtime,
236
+ agentId: node.id,
237
+ agentName: resolved.agentName,
217
238
  // Top-level, in addition to agentExecution.sessionID
218
239
  // below: matches the agent-server node's convention
219
240
  // so this output can be fed straight back into the
@@ -261,13 +282,22 @@ module.exports = function (RED) {
261
282
  node.scheduler = new ExecutionScheduler({
262
283
  concurrency: node.concurrency,
263
284
  onStart: startExecution,
264
- onQueued: (item) => emitEvent(item.send, item.msg, item.executionId, "queued"),
285
+ onQueued: (item) =>
286
+ emitEvent(item.send, item.msg, item.executionId, "queued", item.resolved.agentName),
265
287
  // Runs after this item is removed from `active` and any newly-
266
288
  // eligible queued item has already been started, so the terminal
267
289
  // event's active/queued counts are accurate (see the onStatus
268
290
  // comment in startExecution for why it's deferred to here).
269
291
  onSettled: (item) => {
270
- if (item.finalStatus) emitEvent(item.send, item.msg, item.executionId, item.finalStatus);
292
+ if (item.finalStatus) {
293
+ emitEvent(
294
+ item.send,
295
+ item.msg,
296
+ item.executionId,
297
+ item.finalStatus,
298
+ item.resolved.agentName,
299
+ );
300
+ }
271
301
  updateStatus();
272
302
  },
273
303
  });
@@ -287,6 +317,7 @@ module.exports = function (RED) {
287
317
  return;
288
318
  }
289
319
 
320
+ const agentName = resolveAgentName(msg);
290
321
  const result = node.scheduler.cancel(executionId);
291
322
  if (!result) {
292
323
  done(new Error(`agent: unknown or already-finished executionId "${executionId}"`));
@@ -295,7 +326,13 @@ module.exports = function (RED) {
295
326
 
296
327
  if (result.status === "queued") {
297
328
  const queuedItem = result.item;
298
- emitEvent(queuedItem.send, queuedItem.msg, executionId, "cancelled");
329
+ emitEvent(
330
+ queuedItem.send,
331
+ queuedItem.msg,
332
+ executionId,
333
+ "cancelled",
334
+ queuedItem.resolved ? queuedItem.resolved.agentName : agentName,
335
+ );
299
336
  queuedItem.done(
300
337
  new Error("agent: execution cancelled before it started (terminate requested)"),
301
338
  );
@@ -303,6 +340,8 @@ module.exports = function (RED) {
303
340
  send([
304
341
  Object.assign({}, msg, {
305
342
  payload: { executionId, terminated: true, status: "cancelled" },
343
+ agentId: node.id,
344
+ agentName,
306
345
  }),
307
346
  null,
308
347
  ]);
@@ -320,6 +359,8 @@ module.exports = function (RED) {
320
359
  send([
321
360
  Object.assign({}, msg, {
322
361
  payload: { executionId, terminated: true, status: "terminating" },
362
+ agentId: node.id,
363
+ agentName,
323
364
  }),
324
365
  null,
325
366
  ]);
@@ -354,6 +395,7 @@ module.exports = function (RED) {
354
395
  let resolved;
355
396
  try {
356
397
  resolved = {
398
+ agentName: resolveAgentName(msg),
357
399
  invocation: node.invocation,
358
400
  prompt:
359
401
  node.invocation === "prompt"
@@ -18,6 +18,9 @@
18
18
  defaults: {
19
19
  name: { value: '' },
20
20
 
21
+ serverName: { value: '' },
22
+ serverNameType: { value: 'str' },
23
+
21
24
  operation: { value: 'message' },
22
25
 
23
26
  hostname: { value: '127.0.0.1' },
@@ -71,6 +74,10 @@
71
74
  typeField: '#node-input-modelType',
72
75
  types: ['str', 'msg', 'flow', 'global', 'env']
73
76
  });
77
+ $('#node-input-serverName').typedInput({
78
+ typeField: '#node-input-serverNameType',
79
+ types: ['str', 'msg', 'flow', 'global', 'env']
80
+ });
74
81
 
75
82
  function updateOperationRows() {
76
83
  const op = $('#node-input-operation').val();
@@ -141,6 +148,15 @@
141
148
  <input type="text" id="node-input-name" placeholder="Agent Server">
142
149
  </div>
143
150
 
151
+ <div class="form-row">
152
+ <label for="node-input-serverName"><i class="fa fa-id-badge"></i> Server name
153
+ <i class="fa fa-info-circle" style="color:#888; cursor:help; margin-left:4px;"
154
+ title="Reported as msg.serverName on the result/event outputs. Leave blank (str) to use the Name field above; set to msg/flow/global to make it dynamic per message."></i>
155
+ </label>
156
+ <input type="text" id="node-input-serverName" placeholder="leave blank to use the Name field above" style="width:60%">
157
+ <input type="hidden" id="node-input-serverNameType">
158
+ </div>
159
+
144
160
  <div class="form-row">
145
161
  <label for="node-input-operation"><i class="fa fa-play"></i> Operation</label>
146
162
  <select id="node-input-operation" style="width:70%">
@@ -354,6 +370,16 @@
354
370
  <dd>Operation-dependent: assistant reply text (message), a status object, etc.</dd>
355
371
  <dt>sessionID <span class="property-type">string</span></dt>
356
372
  <dd>Echoed back so a later trigger can reuse the same daemon/session.</dd>
373
+ <dt>serverId <span class="property-type">string</span></dt>
374
+ <dd>This node instance's own Node-RED id, so a flow with
375
+ several agent-server nodes can tell which one produced a
376
+ given message.</dd>
377
+ <dt>serverName <span class="property-type">string</span></dt>
378
+ <dd>The resolved Server name field, falling back to this node
379
+ instance's configured Name if the Server name field is
380
+ blank/unresolved for this message (e.g. via
381
+ <code>msg.serverName</code> when configured as a msg
382
+ property).</dd>
357
383
  </dl>
358
384
  </li>
359
385
  <li>Lifecycle events
@@ -363,6 +389,10 @@
363
389
  <code>failed</code> / <code>closed</code>, each with <code>sessionID</code>,
364
390
  <code>timestamp</code>, and this node instance's live <code>active</code>
365
391
  (busy) / <code>tracked</code> (total) daemon counts.</dd>
392
+ <dt>serverId <span class="property-type">string</span></dt>
393
+ <dd>This node instance's own Node-RED id (see Result output).</dd>
394
+ <dt>serverName <span class="property-type">string</span></dt>
395
+ <dd>The resolved Server name for this event (see Result output).</dd>
366
396
  </dl>
367
397
  </li>
368
398
  </ol>
@@ -21,6 +21,12 @@ module.exports = function (RED) {
21
21
 
22
22
  node.operation = config.operation || "message";
23
23
 
24
+ // Typed-input override of the serverName reported on outgoing/event
25
+ // messages (see emitEvent below). Same typed-input pattern as every
26
+ // other overridable field: blank/unresolved falls back to node.name.
27
+ node.serverName = config.serverName !== undefined ? config.serverName : "";
28
+ node.serverNameType = config.serverNameType || "str";
29
+
24
30
  node.sessionIdProp = config.sessionIdProp !== undefined ? config.sessionIdProp : "sessionID";
25
31
  node.sessionIdPropType = config.sessionIdPropType || "msg";
26
32
 
@@ -105,6 +111,8 @@ module.exports = function (RED) {
105
111
  topic: msg && msg.topic,
106
112
  payload: { type },
107
113
  sessionID,
114
+ serverId: node.id,
115
+ serverName: resolveServerName(msg),
108
116
  active: summary.busy,
109
117
  tracked: summary.total,
110
118
  timestamp: Date.now(),
@@ -122,6 +130,16 @@ module.exports = function (RED) {
122
130
  }
123
131
  }
124
132
 
133
+ // Per-message resolution of the reported serverName: the typed-input
134
+ // serverName/serverNameType field (e.g. msg.serverName) if configured
135
+ // and present, else node.name -- same fallback every other typed-input
136
+ // field has when blank/unresolved. Tolerates a missing msg (emitEvent
137
+ // is sometimes called without one, e.g. from spawnNewInstance's own
138
+ // internal bookkeeping) by resolving against an empty object.
139
+ function resolveServerName(msg) {
140
+ return resolveTyped(node.serverName, node.serverNameType, msg || {}, node.name);
141
+ }
142
+
125
143
  function authOptions() {
126
144
  return node.authUsername || node.authPassword
127
145
  ? { username: node.authUsername, password: node.authPassword }
@@ -236,6 +254,8 @@ module.exports = function (RED) {
236
254
  const resultMsg = Object.assign({}, msg, {
237
255
  payload: extractText(response),
238
256
  sessionID,
257
+ serverId: node.id,
258
+ serverName: resolveServerName(msg),
239
259
  agentServer: {
240
260
  sessionID,
241
261
  host: record && record.host,
@@ -330,7 +350,14 @@ module.exports = function (RED) {
330
350
  if (!sessionID) {
331
351
  // Aggregate across every daemon this node instance is
332
352
  // tracking -- purely local, no network calls.
333
- send([Object.assign({}, msg, { payload: node.registry.summary() }), null]);
353
+ send([
354
+ Object.assign({}, msg, {
355
+ payload: node.registry.summary(),
356
+ serverId: node.id,
357
+ serverName: resolveServerName(msg),
358
+ }),
359
+ null,
360
+ ]);
334
361
  done();
335
362
  return;
336
363
  }
@@ -355,6 +382,8 @@ module.exports = function (RED) {
355
382
  startedAt: record.startedAt,
356
383
  lastUsed: record.lastUsed,
357
384
  },
385
+ serverId: node.id,
386
+ serverName: resolveServerName(msg),
358
387
  }),
359
388
  null,
360
389
  ]);
@@ -387,7 +416,15 @@ module.exports = function (RED) {
387
416
  .then(() => {
388
417
  node.registry.setBusy(sessionID, false);
389
418
  updateStatus();
390
- send([Object.assign({}, msg, { payload: true, sessionID }), null]);
419
+ send([
420
+ Object.assign({}, msg, {
421
+ payload: true,
422
+ sessionID,
423
+ serverId: node.id,
424
+ serverName: resolveServerName(msg),
425
+ }),
426
+ null,
427
+ ]);
391
428
  done();
392
429
  })
393
430
  .catch((err) => done(new Error(`agent-server: abort failed: ${err.message}`)));
@@ -416,7 +453,15 @@ module.exports = function (RED) {
416
453
  ...authOptions(),
417
454
  })
418
455
  .then((history) => {
419
- send([Object.assign({}, msg, { payload: history, sessionID }), null]);
456
+ send([
457
+ Object.assign({}, msg, {
458
+ payload: history,
459
+ sessionID,
460
+ serverId: node.id,
461
+ serverName: resolveServerName(msg),
462
+ }),
463
+ null,
464
+ ]);
420
465
  done();
421
466
  })
422
467
  .catch((err) => done(new Error(`agent-server: history fetch failed: ${err.message}`)));
@@ -450,7 +495,15 @@ module.exports = function (RED) {
450
495
  .then(() => {
451
496
  emitEvent(sessionID, "terminated", msg);
452
497
  updateStatus();
453
- send([Object.assign({}, msg, { payload: true, sessionID }), null]);
498
+ send([
499
+ Object.assign({}, msg, {
500
+ payload: true,
501
+ sessionID,
502
+ serverId: node.id,
503
+ serverName: resolveServerName(msg),
504
+ }),
505
+ null,
506
+ ]);
454
507
  done();
455
508
  })
456
509
  .catch((err) => done(new Error(`agent-server: terminate failed: ${err.message}`)));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tbrandenburg/node-red-agents",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Node-RED nodes for running coding agents (opencode, pi) and GitHub CLI operations from flows.",
5
5
  "keywords": [
6
6
  "node-red",