n8n-nodes-tembory 1.1.39 → 1.1.40

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/README.md CHANGED
@@ -2,7 +2,13 @@
2
2
 
3
3
  Node de memoria operacional da Tembory para agentes de IA no n8n.
4
4
 
5
- Versao atual: `1.1.39`.
5
+ Versao atual: `1.1.40`.
6
+
7
+ ## 1.1.40
8
+
9
+ - Extrai fatos operacionais path aware de inputs e outputs de tools, preservando IDs como `serviceId`, `providerId`, `locationId`, `customerId`, `reservationId` e `bookingId`.
10
+ - Injeta esses fatos em `operationalState`, `actionLedger`, `toolLedger` e `action_directive`.
11
+ - Corrige o risco de o Agent usar `serviceId` antigo de memoria vetorial quando o `check_availabilities` mais recente ja retornou o ID correto para o `Booking`.
6
12
 
7
13
  ## 1.1.39
8
14
 
@@ -489,6 +489,7 @@ const deriveOperationalState = (toolHistory = [], profileFacts = {}, recentMessa
489
489
  const name = String(tool.name || 'unknown_tool');
490
490
  const compactInput = compactToolPayload(safeParseToolPayload(tool.input));
491
491
  const compactResult = maybeToolResult(tool, includeResults);
492
+ const facts = extractToolOperationalFacts(tool);
492
493
  const reason = truncate(String(tool.reason || tool.decision || tool.why || tool.source || 'recorded tool call'), 260);
493
494
  toolCountsByName[name] = (toolCountsByName[name] || 0) + 1;
494
495
  latestByName[name] = {
@@ -496,6 +497,7 @@ const deriveOperationalState = (toolHistory = [], profileFacts = {}, recentMessa
496
497
  ok: tool.ok !== false,
497
498
  at: tool.at || null,
498
499
  input: compactInput,
500
+ facts,
499
501
  result: compactResult,
500
502
  reason,
501
503
  source: tool.source || 'unknown',
@@ -507,6 +509,7 @@ const deriveOperationalState = (toolHistory = [], profileFacts = {}, recentMessa
507
509
  at: tool.at || null,
508
510
  reason,
509
511
  input: compactInput,
512
+ facts,
510
513
  output: compactResult,
511
514
  }));
512
515
  if (tool.ok === false)
@@ -538,6 +541,7 @@ const deriveOperationalState = (toolHistory = [], profileFacts = {}, recentMessa
538
541
  last_successful_tool: successfulTools.length ? {
539
542
  name: successfulTools[successfulTools.length - 1].name,
540
543
  at: successfulTools[successfulTools.length - 1].at || null,
544
+ facts: extractToolOperationalFacts(successfulTools[successfulTools.length - 1]),
541
545
  result: maybeToolResult(successfulTools[successfulTools.length - 1], includeResults),
542
546
  } : null,
543
547
  },
@@ -556,6 +560,7 @@ const deriveActionLedger = (toolHistory = [], maxItems = 20, includeResults = tr
556
560
  status: tool.ok === false ? 'failed' : 'ok',
557
561
  reason: truncate(String(tool.reason || tool.decision || tool.why || tool.source || 'recorded tool call'), 260),
558
562
  input: compactToolPayload(safeParseToolPayload(tool.input)),
563
+ facts: extractToolOperationalFacts(tool),
559
564
  result: maybeToolResult(tool, includeResults),
560
565
  at: tool.at || null,
561
566
  source: tool.source || 'unknown',
@@ -2094,16 +2099,25 @@ const buildActionDirective = ({ workingMemory = {}, operationalState = {} }) =>
2094
2099
  return null;
2095
2100
  const toolState = (operationalState || {}).tool_state || {};
2096
2101
  const latestForTool = ((toolState.latest_by_name || {})[tool]) || null;
2102
+ const recentToolEvidence = pruneByLimit((toolState.recent_sequence || [])
2103
+ .filter((item) => item && item.status !== 'failed')
2104
+ .map((item) => cleanContextValue({
2105
+ tool: item.tool,
2106
+ at: item.at,
2107
+ facts: item.facts,
2108
+ }))
2109
+ .filter((item) => item && item.facts), 6);
2097
2110
  return cleanContextValue({
2098
2111
  required_tool: tool,
2099
2112
  next_expected_action: next,
2113
+ evidence_from_recent_tools: recentToolEvidence,
2100
2114
  tool_state: {
2101
2115
  last_tool: (operationalState || {}).last_tool || null,
2102
2116
  required_tool_last_result: latestForTool,
2103
2117
  counts_by_name: toolState.counts_by_name || {},
2104
2118
  failed_by_name: toolState.failed_by_name || {},
2105
2119
  },
2106
- instruction: `If the agent prompt requires this action, call ${tool} now using tool_state/action_ledger as evidence. Do not infer domain policy from memory; memory only provides prior conversation, decisions, inputs and outputs.`,
2120
+ instruction: `call ${tool} now if the agent prompt requires it. Use tool evidence for exact IDs. Do not complete side effects from memory.`,
2107
2121
  });
2108
2122
  };
2109
2123
  const inferToolGuard = ({ query, recentMessages, toolHistory, vectorMemories }) => {
@@ -2520,6 +2534,152 @@ const normalizeToolResultEnvelope = (value) => {
2520
2534
  }
2521
2535
  return parsed;
2522
2536
  };
2537
+ const normalizeFactKey = (key = '') => String(key || '').toLowerCase().replace(/[^a-z0-9]/g, '');
2538
+ const scalarFactValue = (value) => {
2539
+ if (value === undefined || value === null || value === '')
2540
+ return undefined;
2541
+ if (['string', 'number', 'boolean'].includes(typeof value))
2542
+ return value;
2543
+ return undefined;
2544
+ };
2545
+ const pushCandidate = (facts, bucket, value, path) => {
2546
+ const scalar = scalarFactValue(value);
2547
+ if (scalar === undefined)
2548
+ return;
2549
+ facts[bucket] = facts[bucket] || [];
2550
+ if (!facts[bucket].some((item) => String(item.value) === String(scalar) && item.path === path)) {
2551
+ facts[bucket].push({ value: scalar, path });
2552
+ }
2553
+ };
2554
+ const firstCandidateValue = (items = []) => items.length ? items[0].value : undefined;
2555
+ const extractOperationalFactsFromValue = (value, maxCandidates = 8) => {
2556
+ const parsed = normalizeToolResultEnvelope(value);
2557
+ const facts = {
2558
+ serviceIdCandidates: [],
2559
+ providerIdCandidates: [],
2560
+ locationIdCandidates: [],
2561
+ customerIdCandidates: [],
2562
+ reservationIdCandidates: [],
2563
+ bookingIdCandidates: [],
2564
+ confirmationIdCandidates: [],
2565
+ serviceNameCandidates: [],
2566
+ providerNameCandidates: [],
2567
+ statusCandidates: [],
2568
+ timeCandidates: [],
2569
+ dateCandidates: [],
2570
+ };
2571
+ const seen = new Set();
2572
+ let visited = 0;
2573
+ const visit = (item, path = []) => {
2574
+ if (visited > 220 || item === undefined || item === null)
2575
+ return;
2576
+ if (typeof item !== 'object')
2577
+ return;
2578
+ if (seen.has(item))
2579
+ return;
2580
+ seen.add(item);
2581
+ visited += 1;
2582
+ if (Array.isArray(item)) {
2583
+ item.slice(0, 12).forEach((entry, index) => visit(entry, path.concat(String(index))));
2584
+ return;
2585
+ }
2586
+ for (const [rawKey, rawValue] of Object.entries(item)) {
2587
+ const key = normalizeFactKey(rawKey);
2588
+ const nextPath = path.concat(rawKey);
2589
+ const pathText = nextPath.join('.');
2590
+ const parentText = path.join('.').toLowerCase();
2591
+ const parentSuggestsService = /(^|\.)(service|services|servico|servicos|especialidade|especialidades)(\.|$)/i.test(parentText);
2592
+ const parentSuggestsProvider = /(^|\.)(provider|providers|veterinario|veterinarios|vet|vets|professional|profissional|profissionais)(\.|$)/i.test(parentText);
2593
+ const parentSuggestsLocation = /(^|\.)(location|locations|local|locais|unidade|unidades)(\.|$)/i.test(parentText);
2594
+ if (key === 'serviceid' || key === 'servicoid' || (key === 'id' && parentSuggestsService))
2595
+ pushCandidate(facts, 'serviceIdCandidates', rawValue, pathText);
2596
+ if (key === 'providerid' || key === 'veterinarioid' || key === 'vetid' || (key === 'id' && parentSuggestsProvider))
2597
+ pushCandidate(facts, 'providerIdCandidates', rawValue, pathText);
2598
+ if (key === 'locationid' || key === 'localid' || key === 'unidadeid' || (key === 'id' && parentSuggestsLocation))
2599
+ pushCandidate(facts, 'locationIdCandidates', rawValue, pathText);
2600
+ if (key === 'customerid' || key === 'clienteid')
2601
+ pushCandidate(facts, 'customerIdCandidates', rawValue, pathText);
2602
+ if (key === 'reservationid' || key === 'reservaid')
2603
+ pushCandidate(facts, 'reservationIdCandidates', rawValue, pathText);
2604
+ if (key === 'bookingid' || key === 'appointmentid' || key === 'agendamentoid')
2605
+ pushCandidate(facts, 'bookingIdCandidates', rawValue, pathText);
2606
+ if (key === 'confirmationid')
2607
+ pushCandidate(facts, 'confirmationIdCandidates', rawValue, pathText);
2608
+ if (key === 'servicename' || key === 'servico' || key === 'especialidade' || (key === 'name' && parentSuggestsService))
2609
+ pushCandidate(facts, 'serviceNameCandidates', rawValue, pathText);
2610
+ if (key === 'providername' || key === 'veterinarioname' || key === 'nomeveterinario' || (key === 'name' && parentSuggestsProvider))
2611
+ pushCandidate(facts, 'providerNameCandidates', rawValue, pathText);
2612
+ if (key === 'status' || key === 'state')
2613
+ pushCandidate(facts, 'statusCandidates', rawValue, pathText);
2614
+ if (key === 'horario' || key === 'horarios' || key === 'time' || key === 'start' || key === 'end')
2615
+ pushCandidate(facts, 'timeCandidates', rawValue, pathText);
2616
+ if (key === 'date' || key === 'data' || key === 'datadisponivel')
2617
+ pushCandidate(facts, 'dateCandidates', rawValue, pathText);
2618
+ if (rawValue && typeof rawValue === 'object')
2619
+ visit(rawValue, nextPath);
2620
+ }
2621
+ };
2622
+ visit(parsed);
2623
+ const compactCandidates = (items = []) => items.slice(0, maxCandidates);
2624
+ return cleanContextValue({
2625
+ ids: {
2626
+ serviceId: firstCandidateValue(facts.serviceIdCandidates),
2627
+ providerId: firstCandidateValue(facts.providerIdCandidates),
2628
+ locationId: firstCandidateValue(facts.locationIdCandidates),
2629
+ customerId: firstCandidateValue(facts.customerIdCandidates),
2630
+ reservationId: firstCandidateValue(facts.reservationIdCandidates),
2631
+ bookingId: firstCandidateValue(facts.bookingIdCandidates),
2632
+ confirmationId: firstCandidateValue(facts.confirmationIdCandidates),
2633
+ },
2634
+ labels: {
2635
+ serviceName: firstCandidateValue(facts.serviceNameCandidates),
2636
+ providerName: firstCandidateValue(facts.providerNameCandidates),
2637
+ status: firstCandidateValue(facts.statusCandidates),
2638
+ date: firstCandidateValue(facts.dateCandidates),
2639
+ time: firstCandidateValue(facts.timeCandidates),
2640
+ },
2641
+ candidates: {
2642
+ serviceIds: compactCandidates(facts.serviceIdCandidates),
2643
+ providerIds: compactCandidates(facts.providerIdCandidates),
2644
+ locationIds: compactCandidates(facts.locationIdCandidates),
2645
+ },
2646
+ });
2647
+ };
2648
+ const mergeOperationalFactObjects = (primary = {}, secondary = {}) => {
2649
+ const pick = (path) => {
2650
+ const read = (source) => path.reduce((acc, key) => acc && acc[key] !== undefined ? acc[key] : undefined, source);
2651
+ return read(primary) !== undefined ? read(primary) : read(secondary);
2652
+ };
2653
+ return cleanContextValue({
2654
+ ids: {
2655
+ serviceId: pick(['ids', 'serviceId']),
2656
+ providerId: pick(['ids', 'providerId']),
2657
+ locationId: pick(['ids', 'locationId']),
2658
+ customerId: pick(['ids', 'customerId']),
2659
+ reservationId: pick(['ids', 'reservationId']),
2660
+ bookingId: pick(['ids', 'bookingId']),
2661
+ confirmationId: pick(['ids', 'confirmationId']),
2662
+ },
2663
+ labels: {
2664
+ serviceName: pick(['labels', 'serviceName']),
2665
+ providerName: pick(['labels', 'providerName']),
2666
+ status: pick(['labels', 'status']),
2667
+ date: pick(['labels', 'date']),
2668
+ time: pick(['labels', 'time']),
2669
+ },
2670
+ candidates: {
2671
+ serviceIds: ((primary.candidates || {}).serviceIds || (secondary.candidates || {}).serviceIds || []).slice(0, 8),
2672
+ providerIds: ((primary.candidates || {}).providerIds || (secondary.candidates || {}).providerIds || []).slice(0, 8),
2673
+ locationIds: ((primary.candidates || {}).locationIds || (secondary.candidates || {}).locationIds || []).slice(0, 8),
2674
+ },
2675
+ });
2676
+ };
2677
+ const extractToolOperationalFacts = (tool = {}) => {
2678
+ const inputFacts = extractOperationalFactsFromValue(safeParseToolPayload(tool.input));
2679
+ const outputFacts = extractOperationalFactsFromValue(tool.result);
2680
+ const facts = mergeOperationalFactObjects(outputFacts, inputFacts);
2681
+ return Object.keys(facts || {}).length ? facts : undefined;
2682
+ };
2523
2683
  const compactParsedToolOutputForSideChannel = (parsed) => {
2524
2684
  if (parsed === undefined || parsed === null)
2525
2685
  return undefined;
@@ -2559,6 +2719,7 @@ const compactToolHistoryForAgent = (toolHistory = [], maxItems = 6, includeResul
2559
2719
  at: tool.at,
2560
2720
  reason: truncate(String(tool.reason || tool.decision || tool.why || tool.source || 'recorded tool call'), 140),
2561
2721
  input: truncate(String(tool.input || ''), 500) || undefined,
2722
+ facts: extractToolOperationalFacts(tool),
2562
2723
  result: includeResults ? compactToolResult(tool.result, 1200) : undefined,
2563
2724
  }));
2564
2725
  const cleanContextValue = (value) => {
@@ -2641,7 +2802,7 @@ const compactDecisionStateForAgent = (state = {}) => cleanContextValue({
2641
2802
  const compactOperationalStateForAgent = (state = {}) => {
2642
2803
  const counts = state.tool_counts || {};
2643
2804
  const toolState = state.tool_state || {};
2644
- return {
2805
+ return cleanContextValue({
2645
2806
  last_tool: state.last_tool || null,
2646
2807
  tool_counts: {
2647
2808
  total: counts.total || 0,
@@ -2655,8 +2816,7 @@ const compactOperationalStateForAgent = (state = {}) => {
2655
2816
  last_successful_tool: toolState.last_successful_tool || undefined,
2656
2817
  } : undefined,
2657
2818
  blocked_without_context: state.blocked_without_context || [],
2658
- guidance: (state.guidance || []).slice(0, 3),
2659
- };
2819
+ });
2660
2820
  };
2661
2821
  const compactMemoryCompressionForAgent = (compression = {}) => ({
2662
2822
  turn_summary: (compression.turn_summary || []).slice(-3),
@@ -3428,7 +3588,7 @@ const buildContextMessages = ({ payloadFormat, query, userId, profileFacts, work
3428
3588
  statusAnswerMaterial: sectionValue('status_answer_material'),
3429
3589
  action_directive: directive ? cleanContextValue({
3430
3590
  required_tool: directive.required_tool,
3431
- next_expected_action: directive.next_expected_action,
3591
+ evidence_from_recent_tools: directive.evidence_from_recent_tools,
3432
3592
  instruction: directive.instruction,
3433
3593
  }) : undefined,
3434
3594
  turnBrief: compactTurnBriefForAgent(sectionValue('turn_brief')),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "n8n-nodes-tembory",
3
- "version": "1.1.39",
3
+ "version": "1.1.40",
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",