n8n-nodes-bgos 1.4.3 → 1.6.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/dist/nodes/BGOSAction/BGOSAction.node.js +247 -0
- package/dist/nodes/BGOSAction/handler/eventHandler.d.ts +7 -0
- package/dist/nodes/BGOSAction/handler/eventHandler.js +98 -0
- package/dist/nodes/BGOSAction/techWebhook.d.ts +28 -0
- package/dist/nodes/BGOSAction/techWebhook.js +44 -0
- package/dist/nodes/BgosTrigger/BgosTrigger.node.js +40 -2
- package/dist/package.json +1 -1
- package/package.json +1 -1
|
@@ -46,6 +46,7 @@ class BGOSAction {
|
|
|
46
46
|
{ name: 'Chat', value: 'chat' },
|
|
47
47
|
{ name: 'File', value: 'file' },
|
|
48
48
|
{ name: 'Message', value: 'message' },
|
|
49
|
+
{ name: 'Peer Agent', value: 'peer' },
|
|
49
50
|
],
|
|
50
51
|
default: 'message',
|
|
51
52
|
},
|
|
@@ -199,6 +200,123 @@ class BGOSAction {
|
|
|
199
200
|
default: 'answerCallback',
|
|
200
201
|
required: true,
|
|
201
202
|
},
|
|
203
|
+
// ── Peer Agent operations ─────────────────────────────────────────────
|
|
204
|
+
{
|
|
205
|
+
displayName: 'Operation',
|
|
206
|
+
name: 'operation',
|
|
207
|
+
type: 'options',
|
|
208
|
+
noDataExpression: true,
|
|
209
|
+
displayOptions: { show: { resource: ['peer'] } },
|
|
210
|
+
options: [
|
|
211
|
+
{
|
|
212
|
+
name: 'List Peers',
|
|
213
|
+
value: 'listPeers',
|
|
214
|
+
description: "List the user's other assistants visible to this agent. Each peer carries `introduced` (true if you may message them right now) and `expiresAt` for ephemeral allow-once introductions.",
|
|
215
|
+
action: 'List peers',
|
|
216
|
+
},
|
|
217
|
+
{
|
|
218
|
+
name: 'Send to Peer',
|
|
219
|
+
value: 'sendToPeer',
|
|
220
|
+
description: "Send a message into another assistant's side-thread for the user. Optional waitForReply blocks (deadline polling) until the peer replies. Returns { status, sideThreadChatId, messageId, reply? }. Status='requires_introduction' means the user has not enabled this direction yet.",
|
|
221
|
+
action: 'Send to peer',
|
|
222
|
+
},
|
|
223
|
+
{
|
|
224
|
+
name: 'Mark Side-Thread Complete',
|
|
225
|
+
value: 'markPeerThreadComplete',
|
|
226
|
+
description: 'Write the one-line synthesis summary that flips the SideConversationCard from live to completed-collapsed. Only the chat-owner agent (the one whose primary chat hosts the parent message) may call this.',
|
|
227
|
+
action: 'Mark side-thread complete',
|
|
228
|
+
},
|
|
229
|
+
],
|
|
230
|
+
default: 'listPeers',
|
|
231
|
+
required: true,
|
|
232
|
+
},
|
|
233
|
+
// ── Peer: parameters ──────────────────────────────────────────────────
|
|
234
|
+
{
|
|
235
|
+
displayName: 'Caller Assistant ID',
|
|
236
|
+
name: 'callerAssistantId',
|
|
237
|
+
type: 'string',
|
|
238
|
+
default: '',
|
|
239
|
+
placeholder: '820',
|
|
240
|
+
description: 'The assistant id of THIS agent (the one making the peer call). Required for all peer operations.',
|
|
241
|
+
displayOptions: { show: { resource: ['peer'] } },
|
|
242
|
+
required: true,
|
|
243
|
+
},
|
|
244
|
+
{
|
|
245
|
+
displayName: 'Target Assistant ID',
|
|
246
|
+
name: 'targetAssistantId',
|
|
247
|
+
type: 'string',
|
|
248
|
+
default: '',
|
|
249
|
+
placeholder: '821',
|
|
250
|
+
description: 'The assistant id of the peer being messaged.',
|
|
251
|
+
displayOptions: {
|
|
252
|
+
show: { resource: ['peer'], operation: ['sendToPeer'] },
|
|
253
|
+
},
|
|
254
|
+
required: true,
|
|
255
|
+
},
|
|
256
|
+
{
|
|
257
|
+
displayName: 'Parent Message ID',
|
|
258
|
+
name: 'parentMessageId',
|
|
259
|
+
type: 'string',
|
|
260
|
+
default: '',
|
|
261
|
+
placeholder: '9991',
|
|
262
|
+
description: 'The message id in YOUR chat that anchors the side conversation. The frontend renders the SideConversationCard against this message.',
|
|
263
|
+
displayOptions: {
|
|
264
|
+
show: {
|
|
265
|
+
resource: ['peer'],
|
|
266
|
+
operation: ['sendToPeer', 'markPeerThreadComplete'],
|
|
267
|
+
},
|
|
268
|
+
},
|
|
269
|
+
required: true,
|
|
270
|
+
},
|
|
271
|
+
{
|
|
272
|
+
displayName: 'Text',
|
|
273
|
+
name: 'peerText',
|
|
274
|
+
type: 'string',
|
|
275
|
+
typeOptions: { rows: 4 },
|
|
276
|
+
default: '',
|
|
277
|
+
description: 'Message body to send to the peer.',
|
|
278
|
+
displayOptions: {
|
|
279
|
+
show: { resource: ['peer'], operation: ['sendToPeer'] },
|
|
280
|
+
},
|
|
281
|
+
required: true,
|
|
282
|
+
},
|
|
283
|
+
{
|
|
284
|
+
displayName: 'Wait for Reply',
|
|
285
|
+
name: 'peerWaitForReply',
|
|
286
|
+
type: 'boolean',
|
|
287
|
+
default: false,
|
|
288
|
+
description: 'Block until the peer replies (their reply must include replyToId pointing to your sent message). Default: 60s timeout, max 600s.',
|
|
289
|
+
displayOptions: {
|
|
290
|
+
show: { resource: ['peer'], operation: ['sendToPeer'] },
|
|
291
|
+
},
|
|
292
|
+
},
|
|
293
|
+
{
|
|
294
|
+
displayName: 'Timeout (Seconds)',
|
|
295
|
+
name: 'peerTimeoutSeconds',
|
|
296
|
+
type: 'number',
|
|
297
|
+
default: 60,
|
|
298
|
+
typeOptions: { minValue: 1, maxValue: 600 },
|
|
299
|
+
description: 'How long to wait for the peer reply.',
|
|
300
|
+
displayOptions: {
|
|
301
|
+
show: {
|
|
302
|
+
resource: ['peer'],
|
|
303
|
+
operation: ['sendToPeer'],
|
|
304
|
+
peerWaitForReply: [true],
|
|
305
|
+
},
|
|
306
|
+
},
|
|
307
|
+
},
|
|
308
|
+
{
|
|
309
|
+
displayName: 'Summary',
|
|
310
|
+
name: 'peerSummary',
|
|
311
|
+
type: 'string',
|
|
312
|
+
default: '',
|
|
313
|
+
typeOptions: { rows: 3 },
|
|
314
|
+
description: 'One-line synthesis of what the peer accomplished. Shown to the user as the final state of the SideConversationCard.',
|
|
315
|
+
displayOptions: {
|
|
316
|
+
show: { resource: ['peer'], operation: ['markPeerThreadComplete'] },
|
|
317
|
+
},
|
|
318
|
+
required: true,
|
|
319
|
+
},
|
|
202
320
|
// ── Send a Message: required fields ───────────────────────────────────
|
|
203
321
|
{
|
|
204
322
|
displayName: 'Assistant ID',
|
|
@@ -275,6 +393,66 @@ class BGOSAction {
|
|
|
275
393
|
],
|
|
276
394
|
description: 'Rule: If the message is NOT a direct reply to something the user just typed — cron, scheduler, external webhook, unprompted check-in — use INLINE. If the user just sent a message within the last ~2 min and you want their full attention, MODAL is acceptable. When in doubt, pick INLINE — it never interrupts. Max 6 options for inline.',
|
|
277
395
|
},
|
|
396
|
+
{
|
|
397
|
+
displayName: 'From Agent',
|
|
398
|
+
name: 'fromAgent',
|
|
399
|
+
type: 'collection',
|
|
400
|
+
placeholder: 'Add Field',
|
|
401
|
+
default: {},
|
|
402
|
+
description: 'Set when this message is being sent BY ANOTHER AI AGENT (n8n LLM, external bot, peer BGOS agent) so it renders distinctly in the BGOS chat (cyan bubble + name+avatar header) and the receiving agent\'s webhook payload carries fromAgent + a systemHint telling its LLM "you are talking to another AI, not a human". Hybrid identity: registered peers (peerId or externalId looked up in agent_peers) win; otherwise inline name/color/avatarUrl are used as-is. Sender field stays USER — fromAgent is the third render flag.',
|
|
403
|
+
options: [
|
|
404
|
+
{
|
|
405
|
+
displayName: 'Peer ID',
|
|
406
|
+
name: 'peerId',
|
|
407
|
+
type: 'number',
|
|
408
|
+
default: 0,
|
|
409
|
+
description: 'agent_peers.id from BGOS registry. If set, registry data wins over inline fields.',
|
|
410
|
+
},
|
|
411
|
+
{
|
|
412
|
+
displayName: 'External ID',
|
|
413
|
+
name: 'externalId',
|
|
414
|
+
type: 'string',
|
|
415
|
+
default: '',
|
|
416
|
+
placeholder: 'marketing-agent-001',
|
|
417
|
+
description: 'Stable string id (per BGOS user) used to look up the peer in the registry. Use when you registered the peer by name and don\'t want to track its numeric id.',
|
|
418
|
+
},
|
|
419
|
+
{
|
|
420
|
+
displayName: 'Name',
|
|
421
|
+
name: 'name',
|
|
422
|
+
type: 'string',
|
|
423
|
+
default: '',
|
|
424
|
+
placeholder: 'Marketing Agent',
|
|
425
|
+
description: 'Display name shown in the cyan bubble header. Inline fallback used when no registered peer matches.',
|
|
426
|
+
},
|
|
427
|
+
{
|
|
428
|
+
displayName: 'Color',
|
|
429
|
+
name: 'color',
|
|
430
|
+
type: 'color',
|
|
431
|
+
default: '',
|
|
432
|
+
description: 'Bubble color hex (e.g. #0EA5E9). Inline fallback. Defaults to BGOS cyan when omitted.',
|
|
433
|
+
},
|
|
434
|
+
{
|
|
435
|
+
displayName: 'Avatar URL',
|
|
436
|
+
name: 'avatarUrl',
|
|
437
|
+
type: 'string',
|
|
438
|
+
default: '',
|
|
439
|
+
placeholder: 'https://...',
|
|
440
|
+
description: 'Avatar URL. Inline fallback.',
|
|
441
|
+
},
|
|
442
|
+
{
|
|
443
|
+
displayName: 'Agent Type',
|
|
444
|
+
name: 'type',
|
|
445
|
+
type: 'options',
|
|
446
|
+
default: 'n8n',
|
|
447
|
+
options: [
|
|
448
|
+
{ name: 'n8n workflow', value: 'n8n' },
|
|
449
|
+
{ name: 'BGOS assistant', value: 'bgos' },
|
|
450
|
+
{ name: 'External', value: 'external' },
|
|
451
|
+
],
|
|
452
|
+
description: 'Origin of the agent. Influences the default systemHint sent to the receiving agent.',
|
|
453
|
+
},
|
|
454
|
+
],
|
|
455
|
+
},
|
|
278
456
|
],
|
|
279
457
|
},
|
|
280
458
|
// ── Delete a Message ──────────────────────────────────────────────────
|
|
@@ -412,6 +590,66 @@ class BGOSAction {
|
|
|
412
590
|
default: 2000,
|
|
413
591
|
description: 'How often to check for new messages while waiting (default 2 seconds; minimum 1000).',
|
|
414
592
|
},
|
|
593
|
+
{
|
|
594
|
+
displayName: 'From Agent',
|
|
595
|
+
name: 'fromAgent',
|
|
596
|
+
type: 'collection',
|
|
597
|
+
placeholder: 'Add Field',
|
|
598
|
+
default: {},
|
|
599
|
+
description: 'Identify the sending peer agent so the message renders distinctly (cyan bubble + header) AND the receiving agent\'s webhook payload includes fromAgent + a systemHint. Hybrid: registry (peerId/externalId) > inline (name/color/avatarUrl). The Send-and-Wait poll loop is unaffected — it still reads the next reply from the chat history regardless of fromAgent.',
|
|
600
|
+
options: [
|
|
601
|
+
{
|
|
602
|
+
displayName: 'Peer ID',
|
|
603
|
+
name: 'peerId',
|
|
604
|
+
type: 'number',
|
|
605
|
+
default: 0,
|
|
606
|
+
description: 'agent_peers.id from BGOS registry. Registry wins over inline.',
|
|
607
|
+
},
|
|
608
|
+
{
|
|
609
|
+
displayName: 'External ID',
|
|
610
|
+
name: 'externalId',
|
|
611
|
+
type: 'string',
|
|
612
|
+
default: '',
|
|
613
|
+
placeholder: 'marketing-agent-001',
|
|
614
|
+
description: 'Stable string id used to look up the peer in the registry.',
|
|
615
|
+
},
|
|
616
|
+
{
|
|
617
|
+
displayName: 'Name',
|
|
618
|
+
name: 'name',
|
|
619
|
+
type: 'string',
|
|
620
|
+
default: '',
|
|
621
|
+
placeholder: 'Marketing Agent',
|
|
622
|
+
description: 'Display name. Inline fallback when no registry match.',
|
|
623
|
+
},
|
|
624
|
+
{
|
|
625
|
+
displayName: 'Color',
|
|
626
|
+
name: 'color',
|
|
627
|
+
type: 'color',
|
|
628
|
+
default: '',
|
|
629
|
+
description: 'Bubble color hex e.g. #0EA5E9. Inline fallback.',
|
|
630
|
+
},
|
|
631
|
+
{
|
|
632
|
+
displayName: 'Avatar URL',
|
|
633
|
+
name: 'avatarUrl',
|
|
634
|
+
type: 'string',
|
|
635
|
+
default: '',
|
|
636
|
+
placeholder: 'https://...',
|
|
637
|
+
description: 'Avatar URL. Inline fallback.',
|
|
638
|
+
},
|
|
639
|
+
{
|
|
640
|
+
displayName: 'Agent Type',
|
|
641
|
+
name: 'type',
|
|
642
|
+
type: 'options',
|
|
643
|
+
default: 'n8n',
|
|
644
|
+
options: [
|
|
645
|
+
{ name: 'n8n workflow', value: 'n8n' },
|
|
646
|
+
{ name: 'BGOS assistant', value: 'bgos' },
|
|
647
|
+
{ name: 'External', value: 'external' },
|
|
648
|
+
],
|
|
649
|
+
description: 'Origin of the agent.',
|
|
650
|
+
},
|
|
651
|
+
],
|
|
652
|
+
},
|
|
415
653
|
],
|
|
416
654
|
},
|
|
417
655
|
// ── Ask User Input ────────────────────────────────────────────────────
|
|
@@ -758,6 +996,12 @@ class BGOSAction {
|
|
|
758
996
|
nodeParams.renderMode = additionalFields.renderMode
|
|
759
997
|
? String(additionalFields.renderMode)
|
|
760
998
|
: 'inline';
|
|
999
|
+
// Forward the From Agent collection block (peerId/externalId/
|
|
1000
|
+
// name/color/avatarUrl/type). Backend's hybrid resolver picks
|
|
1001
|
+
// registry > inline; absence falls through to a normal user msg.
|
|
1002
|
+
if (additionalFields.fromAgent && typeof additionalFields.fromAgent === 'object') {
|
|
1003
|
+
nodeParams.fromAgent = additionalFields.fromAgent;
|
|
1004
|
+
}
|
|
761
1005
|
}
|
|
762
1006
|
if (operation === 'deleteMessage') {
|
|
763
1007
|
nodeParams.deleteMessageId = String(this.getNodeParameter('deleteMessageId', i, '') ?? '');
|
|
@@ -781,6 +1025,9 @@ class BGOSAction {
|
|
|
781
1025
|
nodeParams.fileId = String(replyAdditional.fileId ?? '');
|
|
782
1026
|
nodeParams.replyTimeoutSeconds = Number(replyAdditional.replyTimeoutSeconds ?? 600);
|
|
783
1027
|
nodeParams.replyPollIntervalMs = Number(replyAdditional.replyPollIntervalMs ?? 2000);
|
|
1028
|
+
if (replyAdditional.fromAgent && typeof replyAdditional.fromAgent === 'object') {
|
|
1029
|
+
nodeParams.fromAgent = replyAdditional.fromAgent;
|
|
1030
|
+
}
|
|
784
1031
|
}
|
|
785
1032
|
if (operation === 'getChat') {
|
|
786
1033
|
nodeParams.chatId = String(this.getNodeParameter('chatId', i, '') ?? '');
|
|
@@ -41,6 +41,13 @@ type NodeParams = {
|
|
|
41
41
|
isPinned?: boolean;
|
|
42
42
|
replyTimeoutSeconds?: number;
|
|
43
43
|
replyPollIntervalMs?: number;
|
|
44
|
+
callerAssistantId?: string;
|
|
45
|
+
targetAssistantId?: string;
|
|
46
|
+
parentMessageId?: string;
|
|
47
|
+
peerText?: string;
|
|
48
|
+
peerWaitForReply?: boolean;
|
|
49
|
+
peerTimeoutSeconds?: number;
|
|
50
|
+
peerSummary?: string;
|
|
44
51
|
[key: string]: unknown;
|
|
45
52
|
};
|
|
46
53
|
export declare function handleEventByType(this: IExecuteFunctions, eventType: string, eventData: Record<string, unknown>, nodeParams: NodeParams): Promise<unknown>;
|
|
@@ -9,6 +9,38 @@ function getApiOptions(nodeParams) {
|
|
|
9
9
|
}
|
|
10
10
|
return { baseUrl, apiKey: nodeParams.apiKey };
|
|
11
11
|
}
|
|
12
|
+
/**
|
|
13
|
+
* Strip empty values from the `fromAgent` collection so an empty UI block
|
|
14
|
+
* doesn't accidentally trigger the agent-render path on the backend. Returns
|
|
15
|
+
* undefined when there's nothing meaningful to send.
|
|
16
|
+
*/
|
|
17
|
+
function sanitizeFromAgent(raw) {
|
|
18
|
+
if (!raw || typeof raw !== 'object')
|
|
19
|
+
return undefined;
|
|
20
|
+
const obj = raw;
|
|
21
|
+
const peerId = typeof obj.peerId === 'number' && obj.peerId > 0 ? obj.peerId : undefined;
|
|
22
|
+
const externalId = typeof obj.externalId === 'string' && obj.externalId.trim() ? obj.externalId.trim() : undefined;
|
|
23
|
+
const name = typeof obj.name === 'string' && obj.name.trim() ? obj.name.trim() : undefined;
|
|
24
|
+
const color = typeof obj.color === 'string' && obj.color.trim() ? obj.color.trim() : undefined;
|
|
25
|
+
const avatarUrl = typeof obj.avatarUrl === 'string' && obj.avatarUrl.trim() ? obj.avatarUrl.trim() : undefined;
|
|
26
|
+
const type = typeof obj.type === 'string' && obj.type.trim() ? obj.type.trim() : undefined;
|
|
27
|
+
if (!peerId && !externalId && !name && !color && !avatarUrl && !type)
|
|
28
|
+
return undefined;
|
|
29
|
+
const out = {};
|
|
30
|
+
if (peerId !== undefined)
|
|
31
|
+
out.peerId = peerId;
|
|
32
|
+
if (externalId !== undefined)
|
|
33
|
+
out.externalId = externalId;
|
|
34
|
+
if (name !== undefined)
|
|
35
|
+
out.name = name;
|
|
36
|
+
if (color !== undefined)
|
|
37
|
+
out.color = color;
|
|
38
|
+
if (avatarUrl !== undefined)
|
|
39
|
+
out.avatarUrl = avatarUrl;
|
|
40
|
+
if (type !== undefined)
|
|
41
|
+
out.type = type;
|
|
42
|
+
return out;
|
|
43
|
+
}
|
|
12
44
|
async function handleEventByType(eventType, eventData, nodeParams) {
|
|
13
45
|
const operation = nodeParams.operation;
|
|
14
46
|
const assistant = eventData.assistant;
|
|
@@ -54,6 +86,7 @@ async function handleEventByType(eventType, eventData, nodeParams) {
|
|
|
54
86
|
?? eventData.renderMode
|
|
55
87
|
?? eventData.render_mode
|
|
56
88
|
?? 'inline');
|
|
89
|
+
const fromAgent = sanitizeFromAgent(nodeParams.fromAgent);
|
|
57
90
|
return await techWebhook_1.sendMessageToBackend.call(this, apiOptions, {
|
|
58
91
|
assistantId: assistantId,
|
|
59
92
|
chatId: chatId,
|
|
@@ -71,6 +104,7 @@ async function handleEventByType(eventType, eventData, nodeParams) {
|
|
|
71
104
|
audioMimeType: (eventData.audioMimeType ?? message?.audioMimeType ?? null),
|
|
72
105
|
audioDuration: (eventData.audioDuration ?? message?.audioDuration ?? null),
|
|
73
106
|
isMixedAttachments: files.length > 0 && text ? true : (eventData.isMixedAttachments ?? message?.isMixedAttachments ?? null),
|
|
107
|
+
...(fromAgent ? { fromAgent } : {}),
|
|
74
108
|
});
|
|
75
109
|
}
|
|
76
110
|
case 'deleteMessage': {
|
|
@@ -115,7 +149,12 @@ async function handleEventByType(eventType, eventData, nodeParams) {
|
|
|
115
149
|
const fileIdParam = nodeParams.fileId?.trim();
|
|
116
150
|
const files = fileIdParam ? [{ id: fileIdParam }] : [];
|
|
117
151
|
const sender = nodeParams.sender || 'assistant';
|
|
152
|
+
const fromAgentReply = sanitizeFromAgent(nodeParams.fromAgent);
|
|
118
153
|
// Step 1: post the outgoing message and capture its id
|
|
154
|
+
// NOTE: fromAgent flows through here — it's persisted on the request
|
|
155
|
+
// message and forwarded on the receiving agent's webhook payload, but
|
|
156
|
+
// does NOT affect the polling loop below (which only reads sentMessageId
|
|
157
|
+
// and the next message's id+text).
|
|
119
158
|
const sendResult = (await techWebhook_1.sendMessageToBackend.call(this, apiOptions, {
|
|
120
159
|
assistantId: assistantId,
|
|
121
160
|
chatId: chatId,
|
|
@@ -123,6 +162,7 @@ async function handleEventByType(eventType, eventData, nodeParams) {
|
|
|
123
162
|
text,
|
|
124
163
|
files,
|
|
125
164
|
isMixedAttachments: files.length > 0 ? true : null,
|
|
165
|
+
...(fromAgentReply ? { fromAgent: fromAgentReply } : {}),
|
|
126
166
|
}));
|
|
127
167
|
const sentMessageId = Number(sendResult?.id
|
|
128
168
|
?? sendResult?.message?.id
|
|
@@ -457,6 +497,64 @@ async function handleEventByType(eventType, eventData, nodeParams) {
|
|
|
457
497
|
error,
|
|
458
498
|
});
|
|
459
499
|
}
|
|
500
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
501
|
+
// Peer Agent (cross-channel agent-to-agent) — see
|
|
502
|
+
// docs/superpowers/specs/2026-04-30-agent-to-agent-via-bgos-design.md
|
|
503
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
504
|
+
case 'listPeers': {
|
|
505
|
+
const apiOptions = getApiOptions(nodeParams);
|
|
506
|
+
const callerAssistantIdRaw = nodeParams.callerAssistantId ??
|
|
507
|
+
eventData.assistant_id ??
|
|
508
|
+
assistant?.id;
|
|
509
|
+
const callerAssistantId = Number(callerAssistantIdRaw);
|
|
510
|
+
if (!Number.isFinite(callerAssistantId) || callerAssistantId <= 0) {
|
|
511
|
+
throw new Error('Caller Assistant ID is required for List Peers (the assistant id of THIS agent).');
|
|
512
|
+
}
|
|
513
|
+
return await techWebhook_1.listPeers.call(this, apiOptions, callerAssistantId);
|
|
514
|
+
}
|
|
515
|
+
case 'sendToPeer': {
|
|
516
|
+
const apiOptions = getApiOptions(nodeParams);
|
|
517
|
+
const callerAssistantId = Number(nodeParams.callerAssistantId);
|
|
518
|
+
const targetAssistantId = Number(nodeParams.targetAssistantId);
|
|
519
|
+
const parentMessageId = Number(nodeParams.parentMessageId);
|
|
520
|
+
const text = (nodeParams.peerText ?? '').trim();
|
|
521
|
+
if (!Number.isFinite(callerAssistantId) || callerAssistantId <= 0) {
|
|
522
|
+
throw new Error('Caller Assistant ID is required for Send to Peer.');
|
|
523
|
+
}
|
|
524
|
+
if (!Number.isFinite(targetAssistantId) || targetAssistantId <= 0) {
|
|
525
|
+
throw new Error('Target Assistant ID is required for Send to Peer.');
|
|
526
|
+
}
|
|
527
|
+
if (!Number.isFinite(parentMessageId) || parentMessageId <= 0) {
|
|
528
|
+
throw new Error('Parent Message ID is required for Send to Peer — it anchors the SideConversationCard in your chat.');
|
|
529
|
+
}
|
|
530
|
+
if (!text) {
|
|
531
|
+
throw new Error('Text is required for Send to Peer.');
|
|
532
|
+
}
|
|
533
|
+
return await techWebhook_1.sendToPeer.call(this, apiOptions, callerAssistantId, targetAssistantId, {
|
|
534
|
+
text,
|
|
535
|
+
parentMessageId,
|
|
536
|
+
waitForReply: nodeParams.peerWaitForReply === true,
|
|
537
|
+
timeoutSeconds: typeof nodeParams.peerTimeoutSeconds === 'number'
|
|
538
|
+
? nodeParams.peerTimeoutSeconds
|
|
539
|
+
: undefined,
|
|
540
|
+
});
|
|
541
|
+
}
|
|
542
|
+
case 'markPeerThreadComplete': {
|
|
543
|
+
const apiOptions = getApiOptions(nodeParams);
|
|
544
|
+
const callerAssistantId = Number(nodeParams.callerAssistantId);
|
|
545
|
+
const parentMessageId = Number(nodeParams.parentMessageId);
|
|
546
|
+
const summary = (nodeParams.peerSummary ?? '').trim();
|
|
547
|
+
if (!Number.isFinite(callerAssistantId) || callerAssistantId <= 0) {
|
|
548
|
+
throw new Error('Caller Assistant ID is required for Mark Side-Thread Complete.');
|
|
549
|
+
}
|
|
550
|
+
if (!Number.isFinite(parentMessageId) || parentMessageId <= 0) {
|
|
551
|
+
throw new Error('Parent Message ID is required for Mark Side-Thread Complete.');
|
|
552
|
+
}
|
|
553
|
+
if (!summary) {
|
|
554
|
+
throw new Error('Summary is required for Mark Side-Thread Complete — this is the one-line synthesis the user sees in the SideConversationCard.');
|
|
555
|
+
}
|
|
556
|
+
return await techWebhook_1.markPeerThreadComplete.call(this, apiOptions, callerAssistantId, parentMessageId, summary);
|
|
557
|
+
}
|
|
460
558
|
default:
|
|
461
559
|
throw new Error(`Unsupported operation: ${operation}`);
|
|
462
560
|
}
|
|
@@ -146,3 +146,31 @@ export declare function uploadBinaryToS3(this: IExecuteFunctions, uploadUrl: str
|
|
|
146
146
|
* Step 3: Save file metadata to the BGOS backend after a successful S3 upload.
|
|
147
147
|
*/
|
|
148
148
|
export declare function saveFileMeta(this: IExecuteFunctions, options: BgosApiOptions, userId: string, key: string, type: string, size: number): Promise<FileUploadResult>;
|
|
149
|
+
export interface PeerListItem {
|
|
150
|
+
assistantId: number;
|
|
151
|
+
name: string;
|
|
152
|
+
avatarUrl: string | null;
|
|
153
|
+
color: string | null;
|
|
154
|
+
introduced: boolean;
|
|
155
|
+
expiresAt: string | null;
|
|
156
|
+
}
|
|
157
|
+
export interface SendToPeerResult {
|
|
158
|
+
status: 'sent' | 'requires_introduction';
|
|
159
|
+
sideThreadChatId: number | null;
|
|
160
|
+
messageId: number | null;
|
|
161
|
+
reply: {
|
|
162
|
+
messageId: number;
|
|
163
|
+
text: string | null;
|
|
164
|
+
fromAssistantId: number;
|
|
165
|
+
} | null;
|
|
166
|
+
}
|
|
167
|
+
export declare function listPeers(this: IExecuteFunctions, options: BgosApiOptions, callerAssistantId: number): Promise<PeerListItem[]>;
|
|
168
|
+
export declare function sendToPeer(this: IExecuteFunctions, options: BgosApiOptions, callerAssistantId: number, targetAssistantId: number, body: {
|
|
169
|
+
text: string;
|
|
170
|
+
parentMessageId: number;
|
|
171
|
+
waitForReply?: boolean;
|
|
172
|
+
timeoutSeconds?: number;
|
|
173
|
+
}): Promise<SendToPeerResult>;
|
|
174
|
+
export declare function markPeerThreadComplete(this: IExecuteFunctions, options: BgosApiOptions, callerAssistantId: number, parentMessageId: number, summary: string): Promise<{
|
|
175
|
+
ok: true;
|
|
176
|
+
}>;
|
|
@@ -17,6 +17,9 @@ exports.reportCallbackResult = reportCallbackResult;
|
|
|
17
17
|
exports.getUploadUrl = getUploadUrl;
|
|
18
18
|
exports.uploadBinaryToS3 = uploadBinaryToS3;
|
|
19
19
|
exports.saveFileMeta = saveFileMeta;
|
|
20
|
+
exports.listPeers = listPeers;
|
|
21
|
+
exports.sendToPeer = sendToPeer;
|
|
22
|
+
exports.markPeerThreadComplete = markPeerThreadComplete;
|
|
20
23
|
function buildHeaders(apiKey) {
|
|
21
24
|
const headers = { 'Content-Type': 'application/json', Accept: 'application/json' };
|
|
22
25
|
if (apiKey)
|
|
@@ -167,6 +170,12 @@ async function sendMessageToBackend(options, payload) {
|
|
|
167
170
|
};
|
|
168
171
|
if (payload.renderMode)
|
|
169
172
|
body.renderMode = payload.renderMode;
|
|
173
|
+
// Forward the peer-agent identity block when the workflow set it. Backend's
|
|
174
|
+
// hybrid resolver picks registry vs inline; either form makes the BGOS UI
|
|
175
|
+
// render the cyan agent bubble + name header on this message.
|
|
176
|
+
if (payload.fromAgent && typeof payload.fromAgent === 'object') {
|
|
177
|
+
body.fromAgent = payload.fromAgent;
|
|
178
|
+
}
|
|
170
179
|
return this.helpers.httpRequest({
|
|
171
180
|
method: 'POST',
|
|
172
181
|
url,
|
|
@@ -283,3 +292,38 @@ async function saveFileMeta(options, userId, key, type, size) {
|
|
|
283
292
|
json: true,
|
|
284
293
|
});
|
|
285
294
|
}
|
|
295
|
+
function buildPeerHeaders(apiKey, callerAssistantId) {
|
|
296
|
+
const headers = buildHeaders(apiKey);
|
|
297
|
+
headers['X-Caller-Assistant-Id'] = String(callerAssistantId);
|
|
298
|
+
return headers;
|
|
299
|
+
}
|
|
300
|
+
async function listPeers(options, callerAssistantId) {
|
|
301
|
+
const url = `${options.baseUrl.replace(/\/$/, '')}/api/v1/peers`;
|
|
302
|
+
const result = (await this.helpers.httpRequest({
|
|
303
|
+
method: 'GET',
|
|
304
|
+
url,
|
|
305
|
+
headers: buildPeerHeaders(options.apiKey, callerAssistantId),
|
|
306
|
+
json: true,
|
|
307
|
+
}));
|
|
308
|
+
return Array.isArray(result) ? result : [];
|
|
309
|
+
}
|
|
310
|
+
async function sendToPeer(options, callerAssistantId, targetAssistantId, body) {
|
|
311
|
+
const url = `${options.baseUrl.replace(/\/$/, '')}/api/v1/peers/${encodeURIComponent(String(targetAssistantId))}/send`;
|
|
312
|
+
return this.helpers.httpRequest({
|
|
313
|
+
method: 'POST',
|
|
314
|
+
url,
|
|
315
|
+
headers: buildPeerHeaders(options.apiKey, callerAssistantId),
|
|
316
|
+
body,
|
|
317
|
+
json: true,
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
async function markPeerThreadComplete(options, callerAssistantId, parentMessageId, summary) {
|
|
321
|
+
const url = `${options.baseUrl.replace(/\/$/, '')}/api/v1/peers/threads/${encodeURIComponent(String(parentMessageId))}/complete`;
|
|
322
|
+
return this.helpers.httpRequest({
|
|
323
|
+
method: 'POST',
|
|
324
|
+
url,
|
|
325
|
+
headers: buildPeerHeaders(options.apiKey, callerAssistantId),
|
|
326
|
+
body: { summary },
|
|
327
|
+
json: true,
|
|
328
|
+
});
|
|
329
|
+
}
|
|
@@ -133,8 +133,46 @@ class BgosTrigger {
|
|
|
133
133
|
const rawBody = this.getBodyData();
|
|
134
134
|
const updates = this.getNodeParameter('updates', 0);
|
|
135
135
|
const workflowId = String(this.getWorkflow().id ?? 'default');
|
|
136
|
-
if (isRateLimited(workflowId))
|
|
137
|
-
|
|
136
|
+
if (isRateLimited(workflowId)) {
|
|
137
|
+
// Drop the event explicitly with HTTP 429 so the upstream caller
|
|
138
|
+
// (BGOS backend webhook forwarder) sees a real failure and surfaces
|
|
139
|
+
// it in the BGOS forwarder log. Returning empty workflowData with a
|
|
140
|
+
// 200 silently swallowed flood-control drops, which made it
|
|
141
|
+
// impossible to diagnose missing events from a noisy assistant.
|
|
142
|
+
const res = this.getResponseObject();
|
|
143
|
+
try {
|
|
144
|
+
res.status(429).json({
|
|
145
|
+
error: 'rate_limited',
|
|
146
|
+
message: 'BGOS Trigger rate limit exceeded — event dropped.',
|
|
147
|
+
workflowId,
|
|
148
|
+
windowMs: RATE_WINDOW_MS,
|
|
149
|
+
maxEvents: RATE_MAX_EVENTS,
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
catch {
|
|
153
|
+
// Older n8n runtimes lack getResponseObject — fall back to the
|
|
154
|
+
// older shape but still expose the drop in workflowData so users
|
|
155
|
+
// can branch on it instead of silently no-op'ing.
|
|
156
|
+
}
|
|
157
|
+
return {
|
|
158
|
+
webhookResponse: {
|
|
159
|
+
status: 429,
|
|
160
|
+
body: 'rate_limited',
|
|
161
|
+
},
|
|
162
|
+
workflowData: [
|
|
163
|
+
[
|
|
164
|
+
{
|
|
165
|
+
json: {
|
|
166
|
+
error: 'rate_limited',
|
|
167
|
+
workflowId,
|
|
168
|
+
windowMs: RATE_WINDOW_MS,
|
|
169
|
+
maxEvents: RATE_MAX_EVENTS,
|
|
170
|
+
},
|
|
171
|
+
},
|
|
172
|
+
],
|
|
173
|
+
],
|
|
174
|
+
};
|
|
175
|
+
}
|
|
138
176
|
if (!rawBody) {
|
|
139
177
|
return { workflowData: [] };
|
|
140
178
|
}
|
package/dist/package.json
CHANGED