n8n-nodes-tembory 1.1.4 → 1.1.5

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.
@@ -399,6 +399,275 @@ const safeParseToolPayload = (value) => {
399
399
  return text;
400
400
  }
401
401
  };
402
+ const parseNestedToolPayload = (value, depth = 0) => {
403
+ if (depth > 8)
404
+ return value;
405
+ if (typeof value === 'string') {
406
+ const text = value.trim();
407
+ if (!text)
408
+ return '';
409
+ if (!/^[\[{]/.test(text))
410
+ return value;
411
+ try {
412
+ return parseNestedToolPayload(JSON.parse(text), depth + 1);
413
+ }
414
+ catch {
415
+ return value;
416
+ }
417
+ }
418
+ if (Array.isArray(value))
419
+ return value.map((item) => parseNestedToolPayload(item, depth + 1));
420
+ if (!value || typeof value !== 'object')
421
+ return value;
422
+ const out = {};
423
+ for (const [key, item] of Object.entries(value))
424
+ out[key] = parseNestedToolPayload(item, depth + 1);
425
+ return out;
426
+ };
427
+ const collectObjectsDeep = (value, out = [], seen = new Set()) => {
428
+ if (!value || typeof value !== 'object' || seen.has(value))
429
+ return out;
430
+ seen.add(value);
431
+ if (!Array.isArray(value))
432
+ out.push(value);
433
+ for (const item of Array.isArray(value) ? value : Object.values(value))
434
+ collectObjectsDeep(item, out, seen);
435
+ return out;
436
+ };
437
+ const compactObject = (value) => {
438
+ if (Array.isArray(value)) {
439
+ const arr = value.map(compactObject).filter((item) => item !== undefined);
440
+ return arr.length ? arr : undefined;
441
+ }
442
+ if (!value || typeof value !== 'object')
443
+ return value === '' || value === null || value === undefined ? undefined : value;
444
+ const out = {};
445
+ for (const [key, item] of Object.entries(value)) {
446
+ const cleaned = compactObject(item);
447
+ if (cleaned !== undefined)
448
+ out[key] = cleaned;
449
+ }
450
+ return Object.keys(out).length ? out : undefined;
451
+ };
452
+ const normalizeFieldKey = (value = '') => String(value || '').toLowerCase().replace(/[^a-z0-9]/g, '');
453
+ const hasFieldKey = (key, candidates) => candidates.map(normalizeFieldKey).includes(normalizeFieldKey(key));
454
+ const findFieldDeep = (value, keys) => {
455
+ const objects = collectObjectsDeep(value);
456
+ for (const object of objects) {
457
+ for (const [key, item] of Object.entries(object)) {
458
+ if (hasFieldKey(key, keys) && item !== undefined && item !== null && item !== '')
459
+ return item;
460
+ }
461
+ }
462
+ return undefined;
463
+ };
464
+ const readPath = (value, path) => {
465
+ let current = value;
466
+ for (const segment of path) {
467
+ if (!current || typeof current !== 'object')
468
+ return undefined;
469
+ current = current[segment];
470
+ }
471
+ return current;
472
+ };
473
+ const firstPath = (roots, paths) => {
474
+ for (const root of roots) {
475
+ for (const path of paths) {
476
+ const value = readPath(root, path);
477
+ if (value !== undefined && value !== null && value !== '')
478
+ return value;
479
+ }
480
+ }
481
+ return undefined;
482
+ };
483
+ const numberFrom = (value) => {
484
+ if (value === undefined || value === null || value === '')
485
+ return undefined;
486
+ const match = String(value).match(/-?\d+(?:[.,]\d+)?/);
487
+ if (!match)
488
+ return undefined;
489
+ const parsed = Number(match[0].replace(',', '.'));
490
+ return Number.isFinite(parsed) ? parsed : undefined;
491
+ };
492
+ const integerFrom = (value) => {
493
+ const parsed = numberFrom(value);
494
+ return parsed === undefined ? undefined : Math.trunc(parsed);
495
+ };
496
+ const normalizeTimeValue = (value) => {
497
+ const text = String(value || '').trim().toLowerCase();
498
+ const match = /^(\d{1,2})(?:(?::|h)(\d{0,2}))?$/.exec(text);
499
+ if (!match)
500
+ return '';
501
+ const hour = Number(match[1]);
502
+ const minute = match[2] === '' || match[2] === undefined ? 0 : Number(match[2]);
503
+ if (hour < 0 || hour > 23 || minute < 0 || minute > 59)
504
+ return '';
505
+ return `${String(hour).padStart(2, '0')}:${String(minute).padStart(2, '0')}`;
506
+ };
507
+ const isoDateFromParts = (day, month, year, referenceDates = []) => {
508
+ const dd = String(day || '').padStart(2, '0');
509
+ const mm = String(month || '').padStart(2, '0');
510
+ let yyyy = String(year || '');
511
+ if (!yyyy) {
512
+ const matched = (referenceDates || []).find((date) => String(date || '').slice(5, 10) === `${mm}-${dd}`);
513
+ yyyy = matched ? String(matched).slice(0, 4) : String(new Date().getFullYear());
514
+ }
515
+ if (yyyy.length === 2)
516
+ yyyy = `20${yyyy}`;
517
+ return /^\d{4}$/.test(yyyy) && /^\d{2}$/.test(mm) && /^\d{2}$/.test(dd) ? `${yyyy}-${mm}-${dd}` : '';
518
+ };
519
+ const bookingToolName = (name = '') => String(name || '').toLowerCase();
520
+ const serviceNameFromPageContent = (value) => {
521
+ const content = String(value || '').trim();
522
+ if (!content)
523
+ return '';
524
+ return content.split(/\r?\n/).map((line) => line.trim()).find(Boolean) || '';
525
+ };
526
+ const extractAvailabilitySlots = (parsedResult) => {
527
+ const slots = [];
528
+ const roots = Array.isArray(parsedResult) ? parsedResult : [parsedResult];
529
+ for (const root of roots) {
530
+ if (!root || typeof root !== 'object')
531
+ continue;
532
+ const availability = Array.isArray(root.disponibilidade) ? root.disponibilidade : [];
533
+ for (const item of availability) {
534
+ const date = item.dataDisponivel || item.date || item.data || '';
535
+ const providers = Array.isArray(item.providers) ? item.providers : [];
536
+ for (const provider of providers) {
537
+ const providerId = integerFrom(provider.provider_id ?? provider.providerId ?? provider.id);
538
+ const providerName = provider.provider_name || provider.providerName || provider.name || '';
539
+ const horarios = Array.isArray(provider.horarios) ? provider.horarios : [];
540
+ for (const horario of horarios) {
541
+ const time = normalizeTimeValue(horario);
542
+ if (date && time)
543
+ slots.push(compactObject({ date, time, providerId, providerName }));
544
+ }
545
+ }
546
+ }
547
+ }
548
+ return slots.filter(Boolean);
549
+ };
550
+ const extractToolBookingFacts = (toolName = '', input = '', rawResult = '') => {
551
+ const name = bookingToolName(toolName);
552
+ const inputObj = parseNestedToolPayload(safeParseToolPayload(input));
553
+ const parsedResult = parseNestedToolPayload(safeParseToolPayload(rawResult));
554
+ const roots = Array.isArray(parsedResult) ? parsedResult : [parsedResult];
555
+ const objects = collectObjectsDeep(parsedResult);
556
+ const facts = { source_tool: toolName };
557
+ if (/check[_-]?availabilities|disponibilidade/i.test(name)) {
558
+ facts.kind = 'availability';
559
+ facts.locationId = integerFrom(firstPath(roots, [['locationId'], ['location_id']]) ?? findFieldDeep(parsedResult, ['locationId', 'location_id']));
560
+ facts.locationName = inputObj && typeof inputObj === 'object' ? inputObj.location_name || inputObj.locationName : undefined;
561
+ facts.serviceName = firstPath(roots, [['especialidade'], ['serviceName'], ['service_name']]) || (inputObj && typeof inputObj === 'object' ? inputObj.service_name || inputObj.serviceName : undefined);
562
+ facts.serviceId = integerFrom(firstPath(roots, [['services', 'id'], ['service', 'id'], ['serviceId'], ['service_id']]) ?? findFieldDeep(parsedResult, ['serviceId', 'service_id']));
563
+ facts.durationMinutes = integerFrom(firstPath(roots, [['services', 'duration'], ['duration'], ['durationMinutes']]) ?? findFieldDeep(parsedResult, ['duration', 'durationMinutes']));
564
+ facts.price = firstPath(roots, [['services', 'price'], ['price']]) ?? findFieldDeep(parsedResult, ['price', 'price_num']);
565
+ facts.providerId = integerFrom(firstPath(roots, [['provider', 'id'], ['providerId'], ['provider_id']]) ?? findFieldDeep(parsedResult, ['providerId', 'provider_id']));
566
+ facts.availability_slots = extractAvailabilitySlots(parsedResult).slice(0, 80);
567
+ }
568
+ else if (/services?|veterinarios?/i.test(name)) {
569
+ facts.kind = /veterinarios?/i.test(name) ? 'provider_service' : 'service';
570
+ facts.serviceId = integerFrom(firstPath(roots, [['metadata', 'metadata', 'service_id'], ['metadata', 'service_id'], ['service_id'], ['serviceId']]) ?? findFieldDeep(parsedResult, ['service_id', 'serviceId']));
571
+ facts.serviceName = firstPath(roots, [['pageContent']]) || (objects.map((object) => serviceNameFromPageContent(object.pageContent)).find(Boolean));
572
+ facts.serviceName = serviceNameFromPageContent(facts.serviceName || '');
573
+ facts.locationId = integerFrom(firstPath(roots, [['metadata', 'metadata', 'location_id'], ['metadata', 'location_id'], ['locationId'], ['local']]) ?? findFieldDeep(parsedResult, ['location_id', 'locationId', 'local']));
574
+ facts.providerId = integerFrom(firstPath(roots, [['metadata', 'metadata', 'provider_id'], ['metadata', 'provider_id'], ['provider_id'], ['providerId']]) ?? findFieldDeep(parsedResult, ['provider_id', 'providerId']));
575
+ facts.price = firstPath(roots, [['metadata', 'metadata', 'price_num'], ['metadata', 'price_num'], ['price']]) ?? findFieldDeep(parsedResult, ['price_num', 'price']);
576
+ facts.durationMinutes = integerFrom(firstPath(roots, [['metadata', 'metadata', 'duration'], ['metadata', 'duration'], ['duration']]) ?? findFieldDeep(parsedResult, ['duration']));
577
+ }
578
+ else if (/anclivepa.*customer.*upsert|customer.*upsert/i.test(name)) {
579
+ facts.kind = 'booking_customer';
580
+ facts.customerId = integerFrom(firstPath(roots, [['customerId'], ['customer_id']]) ?? findFieldDeep(parsedResult, ['customerId', 'customer_id']));
581
+ facts.petName = firstPath(roots, [['petChosen'], ['petName'], ['pet_name']]) || (inputObj && typeof inputObj === 'object' ? inputObj.petName || inputObj.pet_name : undefined);
582
+ facts.readyForBooking = Boolean(firstPath(roots, [['readyForBooking']]) ?? findFieldDeep(parsedResult, ['readyForBooking']));
583
+ }
584
+ const compacted = compactObject(facts);
585
+ return compacted && Object.keys(compacted).length > 2 ? compacted : undefined;
586
+ };
587
+ const extractBookingSelectionFromMessages = (recentMessages = [], referenceDates = []) => {
588
+ const ordered = [...(recentMessages || [])].reverse();
589
+ for (const msg of ordered) {
590
+ const text = String(msg.content || '');
591
+ const dateTime = /(\d{1,2})[/-](\d{1,2})(?:[/-](\d{2,4}))?[\s\S]{0,160}?(?:às|as|hor[aá]rio)\s*(\d{1,2})(?:(?::|h)(\d{2}))?/i.exec(text);
592
+ if (dateTime) {
593
+ return compactObject({
594
+ date: isoDateFromParts(dateTime[1], dateTime[2], dateTime[3], referenceDates),
595
+ time: normalizeTimeValue(`${dateTime[4]}:${dateTime[5] || '00'}`),
596
+ source: 'recent_message',
597
+ }) || {};
598
+ }
599
+ }
600
+ const dateMsg = ordered.find((msg) => /(\d{1,2})[/-](\d{1,2})(?:[/-](\d{2,4}))?/.test(String(msg.content || '')));
601
+ const timeMsg = ordered.find((msg) => /^(?:\s*(?:às|as)\s*)?\d{1,2}(?:(?::|h)\d{0,2})?\s*$/i.test(String(msg.content || '')));
602
+ const dateMatch = dateMsg ? /(\d{1,2})[/-](\d{1,2})(?:[/-](\d{2,4}))?/.exec(String(dateMsg.content || '')) : null;
603
+ return compactObject({
604
+ date: dateMatch ? isoDateFromParts(dateMatch[1], dateMatch[2], dateMatch[3], referenceDates) : undefined,
605
+ time: timeMsg ? normalizeTimeValue(timeMsg.content) : undefined,
606
+ source: dateMatch || timeMsg ? 'recent_message_partial' : undefined,
607
+ }) || {};
608
+ };
609
+ const addMinutesToSlot = (date, time, minutes = 60) => {
610
+ if (!date || !time)
611
+ return '';
612
+ const start = new Date(`${date}T${time}:00Z`);
613
+ if (!Number.isFinite(start.getTime()))
614
+ return '';
615
+ const end = new Date(start.getTime() + (Number(minutes) || 60) * 60000);
616
+ return `${end.toISOString().slice(0, 10)} ${end.toISOString().slice(11, 19)}`;
617
+ };
618
+ const deriveBookingState = (toolHistory = [], recentMessages = []) => {
619
+ const facts = (toolHistory || []).map((tool) => tool.booking_facts || extractToolBookingFacts(tool.name, tool.input, tool.result)).filter(Boolean);
620
+ if (!facts.length)
621
+ return undefined;
622
+ const latest = (predicate) => [...facts].reverse().find(predicate) || {};
623
+ const availability = latest((fact) => Array.isArray(fact.availability_slots) && fact.availability_slots.length);
624
+ const service = latest((fact) => fact.serviceId);
625
+ const customer = latest((fact) => fact.kind === 'booking_customer' && fact.customerId);
626
+ const slots = availability.availability_slots || [];
627
+ const referenceDates = Array.from(new Set(slots.map((slot) => slot.date).filter(Boolean)));
628
+ const selection = extractBookingSelectionFromMessages(recentMessages, referenceDates);
629
+ const selectedSlot = slots.find((slot) => slot.date === selection.date && slot.time === selection.time) || {};
630
+ const uniqueProviderIds = Array.from(new Set(slots.map((slot) => slot.providerId).filter(Boolean)));
631
+ const durationMinutes = availability.durationMinutes || service.durationMinutes || 60;
632
+ const providerId = selectedSlot.providerId || (uniqueProviderIds.length === 1 ? uniqueProviderIds[0] : availability.providerId || service.providerId);
633
+ const serviceId = availability.serviceId || service.serviceId;
634
+ const locationId = availability.locationId || service.locationId;
635
+ const customerId = customer.customerId;
636
+ const start = selection.date && selection.time ? `${selection.date} ${selection.time}:00` : undefined;
637
+ const end = start ? addMinutesToSlot(selection.date, selection.time, durationMinutes) : undefined;
638
+ const bookingCandidate = compactObject({
639
+ start,
640
+ end,
641
+ date: selection.date,
642
+ time: selection.time,
643
+ providerId,
644
+ providerName: selectedSlot.providerName,
645
+ serviceId,
646
+ serviceName: availability.serviceName || service.serviceName,
647
+ locationId,
648
+ locationName: availability.locationName,
649
+ customerId,
650
+ petName: customer.petName,
651
+ durationMinutes,
652
+ price: availability.price || service.price,
653
+ source: 'derived_from_tool_history',
654
+ }) || {};
655
+ const required = ['providerId', 'serviceId', 'locationId', 'customerId', 'date', 'time'];
656
+ const missing = required.filter((key) => bookingCandidate[key] === undefined || bookingCandidate[key] === null || bookingCandidate[key] === '');
657
+ const warnings = [];
658
+ if (bookingCandidate.providerId && bookingCandidate.locationId && String(bookingCandidate.providerId) === String(bookingCandidate.locationId))
659
+ warnings.push('providerId_equals_locationId');
660
+ if (bookingCandidate.serviceId && bookingCandidate.locationId && String(bookingCandidate.serviceId) === String(bookingCandidate.locationId))
661
+ warnings.push('serviceId_equals_locationId');
662
+ return compactObject({
663
+ booking_candidate: bookingCandidate,
664
+ selected_slot: selectedSlot,
665
+ missing,
666
+ warnings,
667
+ available_slot_count: slots.length || undefined,
668
+ source_tools: Array.from(new Set(facts.map((fact) => fact.source_tool).filter(Boolean))),
669
+ });
670
+ };
402
671
  const compactToolPayload = (value) => truncate(typeof value === 'string' ? value : safeStringify(value), 900);
403
672
  const maybeToolResult = (tool, includeResults = true) => includeResults === false ? undefined : compactToolPayload(safeParseToolPayload(tool === null || tool === void 0 ? void 0 : tool.result));
404
673
  const isToolName = (value = '') => /^[A-Za-z_][A-Za-z0-9_.:-]{1,127}$/.test(String(value || ''));
@@ -438,6 +707,7 @@ const deriveOperationalState = (toolHistory = [], profileFacts = {}, recentMessa
438
707
  at: tool.at || null,
439
708
  input: compactInput,
440
709
  result: compactResult,
710
+ booking_facts: tool.booking_facts,
441
711
  reason,
442
712
  source: tool.source || 'unknown',
443
713
  };
@@ -449,6 +719,7 @@ const deriveOperationalState = (toolHistory = [], profileFacts = {}, recentMessa
449
719
  reason,
450
720
  input: compactInput,
451
721
  output: compactResult,
722
+ booking_facts: tool.booking_facts,
452
723
  }));
453
724
  if (tool.ok === false)
454
725
  failedByName[name] = (failedByName[name] || 0) + 1;
@@ -458,9 +729,11 @@ const deriveOperationalState = (toolHistory = [], profileFacts = {}, recentMessa
458
729
  const guidance = [
459
730
  'Use tool_state and action_ledger as the source of truth for prior tool calls, inputs, outputs, failures and decisions. Domain-specific tool policy must come from the agent prompt, not from memory.',
460
731
  ];
732
+ const bookingState = deriveBookingState(tools, recentMessages);
461
733
  return {
462
734
  profile_complete: Boolean(profileFacts && profileFacts.name && profileFacts.company && profileFacts.email && profileFacts.phone),
463
735
  last_tool: lastTool ? { name: lastTool.name, ok: lastTool.ok, at: lastTool.at } : null,
736
+ booking_state: bookingState,
464
737
  tool_counts: {
465
738
  total: tools.length,
466
739
  ok: successfulTools.length,
@@ -480,6 +753,7 @@ const deriveOperationalState = (toolHistory = [], profileFacts = {}, recentMessa
480
753
  name: successfulTools[successfulTools.length - 1].name,
481
754
  at: successfulTools[successfulTools.length - 1].at || null,
482
755
  result: maybeToolResult(successfulTools[successfulTools.length - 1], includeResults),
756
+ booking_facts: successfulTools[successfulTools.length - 1].booking_facts,
483
757
  } : null,
484
758
  },
485
759
  blocked_without_context: Array.from(new Set(blockedWithoutContext)),
@@ -498,6 +772,7 @@ const deriveActionLedger = (toolHistory = [], maxItems = 20, includeResults = tr
498
772
  reason: truncate(String(tool.reason || tool.decision || tool.why || tool.source || 'recorded tool call'), 260),
499
773
  input: compactToolPayload(safeParseToolPayload(tool.input)),
500
774
  result: maybeToolResult(tool, includeResults),
775
+ booking_facts: tool.booking_facts,
501
776
  at: tool.at || null,
502
777
  source: tool.source || 'unknown',
503
778
  })), maxItems || 20);
@@ -602,6 +877,7 @@ const encodeToolCall = (tool, threadId) => `${TOOL_HISTORY_MARKER}${safeStringif
602
877
  input: tool.input || '',
603
878
  ok: tool.ok !== false,
604
879
  result: tool.result || '',
880
+ booking_facts: tool.booking_facts,
605
881
  at: tool.at || nowIso(),
606
882
  source: tool.source || 'n8n',
607
883
  thread_id: threadId,
@@ -621,6 +897,7 @@ const encodeToolLedger = (tools = [], threadId) => {
621
897
  input: tool.input || '',
622
898
  ok: tool.ok !== false,
623
899
  result: tool.result || '',
900
+ booking_facts: tool.booking_facts,
624
901
  at: tool.at || nowIso(),
625
902
  source: tool.source || 'n8n',
626
903
  })),
@@ -647,6 +924,7 @@ const encodeTurnArchive = ({ threadId, messages = [], tools = [], workingMemory
647
924
  input: tool.input || '',
648
925
  ok: tool.ok !== false,
649
926
  output: tool.result || '',
927
+ booking_facts: tool.booking_facts,
650
928
  at: tool.at || nowIso(),
651
929
  source: tool.source || 'n8n',
652
930
  })),
@@ -688,6 +966,7 @@ const parseToolHistoryMarker = (text) => {
688
966
  input: parsed.input === undefined ? '' : String(parsed.input),
689
967
  ok: parsed.ok !== false,
690
968
  result: truncate(parsed.result || '', 1000),
969
+ booking_facts: parsed.booking_facts || parsed.bookingFacts,
691
970
  at: parsed.at || nowIso(),
692
971
  source: parsed.source || 'tembory_marker',
693
972
  };
@@ -715,6 +994,7 @@ const parseToolLedgerMarker = (text) => {
715
994
  input: tool.input === undefined ? '' : String(tool.input),
716
995
  ok: tool.ok !== false,
717
996
  result: truncate(tool.result || '', 1000),
997
+ booking_facts: tool.booking_facts || tool.bookingFacts,
718
998
  at: tool.at || parsed.generated_at || nowIso(),
719
999
  source: tool.source || 'tool_ledger_marker',
720
1000
  }));
@@ -818,6 +1098,7 @@ const toolHistoryFromMemory = (item) => {
818
1098
  input: truncate((factMatch[2] || '').trim(), 1000),
819
1099
  ok: true,
820
1100
  result: truncate((factMatch[3] || '').trim(), 1000),
1101
+ booking_facts: meta.booking_facts || meta.bookingFacts,
821
1102
  at: meta.at || item.created_at || item.createdAt || nowIso(),
822
1103
  source: 'semantic_fact',
823
1104
  };
@@ -847,6 +1128,7 @@ const toolHistoryFromMemory = (item) => {
847
1128
  input: meta.input === undefined ? '' : String(meta.input),
848
1129
  ok: meta.ok !== false,
849
1130
  result: truncate(meta.result || content || '', 1000),
1131
+ booking_facts: meta.booking_facts || meta.bookingFacts,
850
1132
  at: meta.at || item.created_at || item.createdAt || nowIso(),
851
1133
  source: meta.source || 'metadata',
852
1134
  };
@@ -866,6 +1148,7 @@ const explicitToolHistoryItemsFromMemory = (item) => {
866
1148
  input: tool.input === undefined ? '' : String(tool.input),
867
1149
  ok: tool.ok !== false,
868
1150
  result: truncate(tool.output || tool.result || '', 1000),
1151
+ booking_facts: tool.booking_facts || tool.bookingFacts,
869
1152
  at: tool.at || archive.generated_at || nowIso(),
870
1153
  source: tool.source || 'turn_archive',
871
1154
  }));
@@ -930,7 +1213,9 @@ const canonicalToolInput = (value) => {
930
1213
  const normalizeToolCall = (tool, sequence = 0, defaults = {}) => {
931
1214
  const at = tool.at || defaults.at || nowIso();
932
1215
  const input = canonicalToolInput(tool.input);
933
- const result = summarizeToolResult(tool.result || tool.output || tool.observation || '');
1216
+ const rawResult = tool.result || tool.output || tool.observation || '';
1217
+ const result = summarizeToolResult(rawResult);
1218
+ const bookingFacts = tool.booking_facts || tool.bookingFacts || extractToolBookingFacts(tool.name || tool.tool || tool.toolName || '', input, rawResult);
934
1219
  const normalized = {
935
1220
  id: tool.id || tool.callId || tool.call_id || '',
936
1221
  call_id: tool.id || tool.callId || tool.call_id || '',
@@ -945,11 +1230,14 @@ const normalizeToolCall = (tool, sequence = 0, defaults = {}) => {
945
1230
  ok: tool.ok !== false,
946
1231
  result,
947
1232
  result_summary: result,
1233
+ booking_facts: bookingFacts,
948
1234
  at,
949
1235
  timestamp: at,
950
1236
  source: tool.source || defaults.source || 'n8n',
951
1237
  status: tool.ok === false ? 'failed' : 'ok',
952
1238
  };
1239
+ if (!normalized.booking_facts)
1240
+ delete normalized.booking_facts;
953
1241
  if (!normalized.id)
954
1242
  normalized.id = makeToolEventId(normalized, normalized.sequence);
955
1243
  if (!normalized.call_id)
@@ -1026,8 +1314,12 @@ const extractToolCallsFromMessages = (messages = []) => {
1026
1314
  const name = message.name || message.toolName || message.tool || additional.name || additional.tool_name || additional.toolName || kwargs.name || 'tool';
1027
1315
  const existing = id ? toolCallById.get(String(id)) : null;
1028
1316
  if (existing) {
1029
- existing.result = summarizeToolResult(messageContentOf(message));
1317
+ const rawContent = messageContentOf(message);
1318
+ existing.result = summarizeToolResult(rawContent);
1030
1319
  existing.result_summary = existing.result;
1320
+ existing.booking_facts = extractToolBookingFacts(existing.name, existing.input, rawContent) || existing.booking_facts;
1321
+ if (!existing.booking_facts)
1322
+ delete existing.booking_facts;
1031
1323
  existing.ok = true;
1032
1324
  existing.status = 'ok';
1033
1325
  existing.result_hash = stableHash(existing.result || '');
@@ -1956,7 +2248,7 @@ const contextMemoryText = (memory, max = 700) => {
1956
2248
  };
1957
2249
  const approxTokenCount = (text) => Math.ceil(String(text || '').length / 4);
1958
2250
  const noisyToolFields = new Set(['requested_from', 'thread', 'thread_id', 'run_id', 'dedupe_key', 'args_hash', 'result_hash', 'source']);
1959
- const importantJsonFields = ['id', 'status', 'state', 'next_step', 'action', 'intent', 'tool', 'input', 'output', 'result', 'error', 'message', 'reason', 'customer_id', 'user_id', 'lead_id', 'ticket_id', 'charge_id', 'reservation_id', 'confirmation_id', 'selected_from_message', 'available_slots', 'timezone', 'name', 'phone', 'segmento', 'platform', 'plataforma', 'lifecycle_stage', 'provider', 'sku', 'product', 'stock', 'queue', 'priority'];
2251
+ const importantJsonFields = ['id', 'status', 'state', 'next_step', 'action', 'intent', 'tool', 'input', 'output', 'result', 'error', 'message', 'reason', 'customer_id', 'customerId', 'user_id', 'lead_id', 'ticket_id', 'charge_id', 'reservation_id', 'confirmation_id', 'selected_from_message', 'available_slots', 'availability_slots', 'disponibilidade', 'dataDisponivel', 'horarios', 'timezone', 'name', 'phone', 'segmento', 'platform', 'plataforma', 'lifecycle_stage', 'provider', 'provider_id', 'providerId', 'provider_name', 'services', 'service_id', 'serviceId', 'locationId', 'location_id', 'local', 'especialidade', 'duration', 'price', 'price_num', 'sku', 'product', 'stock', 'queue', 'priority'];
1960
2252
  const stripNoisyToolFields = (value, depth = 0) => {
1961
2253
  if (depth > 8)
1962
2254
  return undefined;
@@ -2028,6 +2320,7 @@ const compactToolHistoryForAgent = (toolHistory = [], maxItems = 6, includeResul
2028
2320
  reason: truncate(String(tool.reason || tool.decision || tool.why || tool.source || 'recorded tool call'), 140),
2029
2321
  input: truncate(String(tool.input || ''), 140) || undefined,
2030
2322
  result: includeResults ? compactToolResult(tool.result, 240) : undefined,
2323
+ booking_facts: tool.booking_facts,
2031
2324
  }));
2032
2325
  const cleanContextValue = (value) => {
2033
2326
  if (Array.isArray(value)) {
@@ -2084,6 +2377,7 @@ const compactOperationalStateForAgent = (state = {}) => {
2084
2377
  const toolState = state.tool_state || {};
2085
2378
  return {
2086
2379
  last_tool: state.last_tool || null,
2380
+ booking_state: state.booking_state,
2087
2381
  tool_counts: {
2088
2382
  total: counts.total || 0,
2089
2383
  ok: counts.ok || 0,
@@ -2112,6 +2406,7 @@ const compactActionLedgerForAgent = (ledger = [], maxItems = 6, includeResults =
2112
2406
  reason: item.reason,
2113
2407
  input: item.input,
2114
2408
  result: includeResults ? compactToolResult(item.result, 180) : undefined,
2409
+ booking_facts: item.booking_facts,
2115
2410
  }));
2116
2411
  const compactEntityTimelineForAgent = (timeline = [], maxItems = 8) => pruneByLimit(timeline || [], maxItems).map((item) => cleanContextValue({
2117
2412
  entity: item.entity || item.source || item.name,
@@ -2420,6 +2715,7 @@ const buildContextMessages = ({ payloadFormat, query, userId, profileFacts, work
2420
2715
  const vectorFacts = sectionValue('summary');
2421
2716
  const slmSummary = sectionValue('connected_model_summary') || sectionValue('active_summary');
2422
2717
  const directive = sectionValue('action_directive');
2718
+ const bookingState = (operationalState || {}).booking_state;
2423
2719
  const inferredIntent = deriveUserIntentObservation({ query, workingMemory, decisionState, recentMessages });
2424
2720
  const minimalState = cleanContextValue({
2425
2721
  next_expected_action: directive ? undefined : workingMemory.next_expected_action,
@@ -2445,6 +2741,7 @@ const buildContextMessages = ({ payloadFormat, query, userId, profileFacts, work
2445
2741
  slm: hasToolLedger ? undefined : slmSummary,
2446
2742
  },
2447
2743
  state: minimalState,
2744
+ booking: bookingState,
2448
2745
  profile: sectionValue('profile_facts'),
2449
2746
  tools: compactToolLedger,
2450
2747
  });
@@ -3170,6 +3467,7 @@ class TemboryMemory {
3170
3467
  input: tool.input,
3171
3468
  ok: tool.ok,
3172
3469
  result: tool.result,
3470
+ booking_facts: tool.booking_facts,
3173
3471
  at: tool.at,
3174
3472
  source: tool.source || 'tembory_transcript',
3175
3473
  thread_id: threadId,
@@ -3897,6 +4195,7 @@ class TemboryMemory {
3897
4195
  dedupe_key: tool.dedupe_key || toolEventKey(tool),
3898
4196
  result: adv.includeToolResults === false ? undefined : tool.result,
3899
4197
  result_summary: adv.includeToolResults === false ? undefined : (tool.result_summary || tool.result),
4198
+ booking_facts: tool.booking_facts,
3900
4199
  })),
3901
4200
  },
3902
4201
  diagnostics,
@@ -4060,6 +4359,8 @@ exports.__private = {
4060
4359
  embedQueryCached,
4061
4360
  compactToolResult,
4062
4361
  compactToolHistoryForAgent,
4362
+ extractToolBookingFacts,
4363
+ deriveBookingState,
4063
4364
  compactVectorMemoriesForAgent,
4064
4365
  isConversationEchoMemory,
4065
4366
  compactOperationalStateForAgent,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "n8n-nodes-tembory",
3
- "version": "1.1.4",
3
+ "version": "1.1.5",
4
4
  "description": "Tembory node for n8n AI Agents with operational memory, tool history and decision state",
5
5
  "license": "MIT",
6
6
  "homepage": "https://tembory.com",