n8n-nodes-tembory 1.0.31 → 1.0.32

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.
@@ -295,7 +295,7 @@ const extractProfileFactsFromText = (text, source = 'message', at = nowIso()) =>
295
295
  const canExtractStrongProfileFacts = sourceName !== 'assistant_message';
296
296
  if (!content.trim())
297
297
  return facts;
298
- if (/\[Used tools:|Calling\s+agenda_|"tool"\s*:|"args"\s*:|confirmation_id|reservation_id/i.test(content))
298
+ if (/\[Used tools:|Calling\s+[A-Za-z_][A-Za-z0-9_.:-]*|"tool"\s*:|"args"\s*:|confirmation_id|reservation_id/i.test(content))
299
299
  return facts;
300
300
  const email = /\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/i.exec(content);
301
301
  if (email && canExtractStrongProfileFacts)
@@ -407,74 +407,63 @@ const safeParseToolPayload = (value) => {
407
407
  };
408
408
  const compactToolPayload = (value) => truncate(typeof value === 'string' ? value : safeStringify(value), 900);
409
409
  const maybeToolResult = (tool, includeResults = true) => includeResults === false ? undefined : compactToolPayload(safeParseToolPayload(tool === null || tool === void 0 ? void 0 : tool.result));
410
- const containsSelectedSlot = (text = '') => /\b(?:às|as|das|para)?\s*\d{1,2}(?::\d{2}|h\d{0,2})?\b/i.test(String(text || '')) || /\b\d{1,2}\/\d{1,2}(?:\/\d{2,4})?\b/.test(String(text || ''));
411
- const availabilityFromRecentMessages = (recentMessages = []) => {
412
- const messages = Array.isArray(recentMessages) ? recentMessages : [];
413
- const candidates = [...messages].reverse().filter((message) => {
414
- const role = String(message.role || '').toLowerCase();
415
- const text = String(message.content || message.text || message.memory || '');
416
- if (role && role !== 'assistant' && role !== 'ai' && role !== 'system')
417
- return false;
418
- return /\b(?:tem|tenho|há|ha)\s+vagas?\b|\bop[][]es?\s+dispon[ií]veis\b|\bavailable_slots\b/i.test(text)
419
- && (/\b\d{1,2}\/\d{1,2}(?:\/\d{2,4})?\b/.test(text) || /\b\d{1,2}:\d{2}\b/.test(text));
420
- });
421
- const latest = candidates[0];
422
- if (!latest)
423
- return null;
424
- return {
425
- text: truncate(String(latest.content || latest.text || latest.memory || ''), 900),
426
- at: latest.at || latest.created_at || latest.createdAt || nowIso(),
427
- source: latest.source || 'recent_message',
428
- };
429
- };
430
- const preReservationFromRecentMessages = (recentMessages = []) => {
431
- const messages = Array.isArray(recentMessages) ? recentMessages : [];
432
- const candidates = [...messages].reverse().filter((message) => {
433
- const role = String(message.role || '').toLowerCase();
434
- const text = String(message.content || message.text || message.memory || '');
435
- if (role && role !== 'assistant' && role !== 'ai' && role !== 'system')
436
- return false;
437
- return /\b(pr[eé][-\s]?reservad[oa]|pre[-\s]?reservad[oa]|reservation_id|RES-[A-Z0-9-]+)\b/i.test(text)
438
- && !/\b(confirmad[oa]|confirmation_id|CONF-[A-Z0-9-]+)\b/i.test(text);
439
- });
440
- const latest = candidates[0];
441
- if (!latest)
442
- return null;
443
- return {
444
- text: truncate(String(latest.content || latest.text || latest.memory || ''), 900),
445
- at: latest.at || latest.created_at || latest.createdAt || nowIso(),
446
- source: latest.source || 'recent_message',
447
- };
410
+ const isToolName = (value = '') => /^[A-Za-z_][A-Za-z0-9_.:-]{1,127}$/.test(String(value || ''));
411
+ const pickRequiredToolFromAction = (text = '') => {
412
+ const next = String(text || '');
413
+ const protectedCall = /\bdo not call\s+([A-Za-z_][A-Za-z0-9_.:-]{1,127})\b[\s\S]*?\bcall\s+([A-Za-z_][A-Za-z0-9_.:-]{1,127})\b/i.exec(next);
414
+ if (protectedCall && isToolName(protectedCall[2]))
415
+ return protectedCall[2];
416
+ if (/\bdo not call\b/i.test(next))
417
+ return '';
418
+ const mustCall = /\bMUST\s+call\s+([A-Za-z_][A-Za-z0-9_.:-]{1,127})\b/i.exec(next);
419
+ if (mustCall && isToolName(mustCall[1]))
420
+ return mustCall[1];
421
+ const call = /\bcall\s+([A-Za-z_][A-Za-z0-9_.:-]{1,127})\b/i.exec(next);
422
+ if (call && isToolName(call[1]))
423
+ return call[1];
424
+ return '';
448
425
  };
449
426
  const deriveOperationalState = (toolHistory = [], profileFacts = {}, recentMessages = [], includeResults = true) => {
450
427
  const tools = Array.isArray(toolHistory) ? toolHistory : [];
451
428
  const successfulTools = tools.filter((tool) => tool.ok !== false);
452
- const byName = (name) => successfulTools.filter((tool) => tool.name === name);
453
- const availability = byName('agenda_consultar_disponibilidade');
454
- const recentAvailability = availability.length ? null : availabilityFromRecentMessages(recentMessages);
455
- const hasAvailability = availability.length > 0 || Boolean(recentAvailability);
456
- const reservations = byName('agenda_pre_reservar_horario');
457
- const recentPreReservation = reservations.length ? null : preReservationFromRecentMessages(recentMessages);
458
- const hasPreReservation = reservations.length > 0 || Boolean(recentPreReservation);
459
- const confirmations = byName('agenda_confirmar_agendamento');
429
+ const failedTools = tools.filter((tool) => tool.ok === false);
430
+ const toolCountsByName = {};
431
+ const latestByName = {};
432
+ const failedByName = {};
433
+ const recentSequence = [];
434
+ for (let index = 0; index < tools.length; index += 1) {
435
+ const tool = tools[index];
436
+ const name = String(tool.name || 'unknown_tool');
437
+ const compactInput = compactToolPayload(safeParseToolPayload(tool.input));
438
+ const compactResult = maybeToolResult(tool, includeResults);
439
+ const reason = truncate(String(tool.reason || tool.decision || tool.why || tool.source || 'recorded tool call'), 260);
440
+ toolCountsByName[name] = (toolCountsByName[name] || 0) + 1;
441
+ latestByName[name] = {
442
+ name,
443
+ ok: tool.ok !== false,
444
+ at: tool.at || null,
445
+ input: compactInput,
446
+ result: compactResult,
447
+ reason,
448
+ source: tool.source || 'unknown',
449
+ };
450
+ recentSequence.push(cleanContextValue({
451
+ sequence: tool.sequence || index + 1,
452
+ tool: name,
453
+ status: tool.ok === false ? 'failed' : 'ok',
454
+ at: tool.at || null,
455
+ reason,
456
+ input: compactInput,
457
+ output: compactResult,
458
+ }));
459
+ if (tool.ok === false)
460
+ failedByName[name] = (failedByName[name] || 0) + 1;
461
+ }
460
462
  const lastTool = tools[tools.length - 1] || null;
461
- const lastReservation = reservations[reservations.length - 1] || null;
462
- const lastConfirmation = confirmations[confirmations.length - 1] || null;
463
- const hasPendingReservation = Boolean((lastReservation && (!lastConfirmation || String(lastReservation.at || '') > String(lastConfirmation.at || ''))) || (recentPreReservation && !lastConfirmation));
464
463
  const blockedWithoutContext = [];
465
- if (!hasAvailability)
466
- blockedWithoutContext.push('agenda_pre_reservar_horario');
467
- if (!hasPreReservation)
468
- blockedWithoutContext.push('agenda_confirmar_agendamento');
469
- const guidance = [];
470
- if (!hasAvailability)
471
- guidance.push('No availability result is known for this session; consult availability before reserving or confirming.');
472
- else if (hasPendingReservation)
473
- guidance.push('A pre-reservation exists after the latest confirmation; confirmation can use the latest pre-reservation context.');
474
- else if (lastConfirmation)
475
- guidance.push('The latest reservation appears confirmed; do not confirm again unless the user explicitly asks to repeat or change it.');
476
- else if (hasAvailability)
477
- guidance.push('Availability is known; if the user chooses one listed slot, reserve without repeating availability.');
464
+ const guidance = [
465
+ '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.',
466
+ ];
478
467
  return {
479
468
  profile_complete: Boolean(profileFacts && profileFacts.name && profileFacts.company && profileFacts.email && profileFacts.phone),
480
469
  last_tool: lastTool ? { name: lastTool.name, ok: lastTool.ok, at: lastTool.at } : null,
@@ -482,20 +471,22 @@ const deriveOperationalState = (toolHistory = [], profileFacts = {}, recentMessa
482
471
  total: tools.length,
483
472
  ok: successfulTools.length,
484
473
  failed: tools.length - successfulTools.length,
485
- agenda_consultar_disponibilidade: availability.length,
486
- agenda_pre_reservar_horario: reservations.length,
487
- agenda_confirmar_agendamento: confirmations.length,
474
+ by_name: toolCountsByName,
488
475
  },
489
- agenda_state: {
490
- has_availability: hasAvailability,
491
- has_pre_reservation: hasPreReservation,
492
- has_confirmation: confirmations.length > 0,
493
- has_pending_pre_reservation: hasPendingReservation,
494
- latest_availability_result: availability.length ? maybeToolResult(availability[availability.length - 1], includeResults) : (recentAvailability && includeResults !== false ? recentAvailability.text : null),
495
- latest_availability_source: availability.length ? 'tool_history' : (recentAvailability ? recentAvailability.source : null),
496
- latest_pre_reservation_result: lastReservation ? maybeToolResult(lastReservation, includeResults) : (recentPreReservation && includeResults !== false ? recentPreReservation.text : null),
497
- latest_pre_reservation_source: lastReservation ? 'tool_history' : (recentPreReservation ? recentPreReservation.source : null),
498
- latest_confirmation_result: lastConfirmation ? maybeToolResult(lastConfirmation, includeResults) : null,
476
+ tool_state: {
477
+ total: tools.length,
478
+ ok: successfulTools.length,
479
+ failed: failedTools.length,
480
+ names: Object.keys(toolCountsByName),
481
+ counts_by_name: toolCountsByName,
482
+ failed_by_name: failedByName,
483
+ latest_by_name: latestByName,
484
+ recent_sequence: pruneByLimit(recentSequence, 20),
485
+ last_successful_tool: successfulTools.length ? {
486
+ name: successfulTools[successfulTools.length - 1].name,
487
+ at: successfulTools[successfulTools.length - 1].at || null,
488
+ result: maybeToolResult(successfulTools[successfulTools.length - 1], includeResults),
489
+ } : null,
499
490
  },
500
491
  blocked_without_context: Array.from(new Set(blockedWithoutContext)),
501
492
  guidance,
@@ -510,6 +501,7 @@ const deriveActionLedger = (toolHistory = [], maxItems = 20, includeResults = tr
510
501
  kind: 'tool_call',
511
502
  name: tool.name || 'unknown_tool',
512
503
  status: tool.ok === false ? 'failed' : 'ok',
504
+ reason: truncate(String(tool.reason || tool.decision || tool.why || tool.source || 'recorded tool call'), 260),
513
505
  input: compactToolPayload(safeParseToolPayload(tool.input)),
514
506
  result: maybeToolResult(tool, includeResults),
515
507
  at: tool.at || null,
@@ -686,7 +678,7 @@ const toolHistoryFromMemory = (item) => {
686
678
  source: 'semantic_fact',
687
679
  };
688
680
  }
689
- const namedTool = meta.kind === 'tool_history' ? /(?:tool|ferramenta)\s+([A-Za-z0-9_.:-]*(?:agenda_[A-Za-z0-9_.:-]+)[A-Za-z0-9_.:-]*)/i.exec(String(content || '')) : null;
681
+ const namedTool = meta.kind === 'tool_history' ? /(?:tool|ferramenta)\s+([A-Za-z_][A-Za-z0-9_.:-]{1,127})/i.exec(String(content || '')) : null;
690
682
  if (namedTool) {
691
683
  return {
692
684
  id: meta.id || meta.call_id || meta.callId || '',
@@ -699,26 +691,6 @@ const toolHistoryFromMemory = (item) => {
699
691
  source: 'semantic_named_tool',
700
692
  };
701
693
  }
702
- const text = meta.kind === 'tool_history' ? String(content || '').toLowerCase() : '';
703
- const inferredAgendaTool = text.includes('confirm') && text.includes('agendamento')
704
- ? 'agenda_confirmar_agendamento'
705
- : text.includes('pré-reserva') || text.includes('pre-reserva') || text.includes('pre reserva') || text.includes('pré reserva')
706
- ? 'agenda_pre_reservar_horario'
707
- : (text.includes('horários disponíveis') || text.includes('horarios disponiveis') || text.includes('available') || text.includes('disponibilidade')) && text.includes('agenda')
708
- ? 'agenda_consultar_disponibilidade'
709
- : '';
710
- if (inferredAgendaTool) {
711
- return {
712
- id: meta.id || meta.call_id || meta.callId || '',
713
- turnId: meta.turn_id || meta.turnId || '',
714
- name: inferredAgendaTool,
715
- input: '',
716
- ok: true,
717
- result: truncate(content || '', 1000),
718
- at: meta.at || item.created_at || item.createdAt || nowIso(),
719
- source: 'semantic_inference',
720
- };
721
- }
722
694
  if (meta.kind !== 'tool_history')
723
695
  return null;
724
696
  const name = meta.name || meta.tool || meta.toolName;
@@ -1268,17 +1240,18 @@ const normalizeIntentText = (value = '') => String(value || '')
1268
1240
  .toLowerCase()
1269
1241
  .normalize('NFD')
1270
1242
  .replace(/[\u0300-\u036f]/g, '');
1271
- const hasConfirmIntent = (value = '') => /\b(confirm\w*|confim\w*|cnfirm\w*|cofnirm\w*|ocnfi\w*|ocnfia\w*|fechar)\b/.test(normalizeIntentText(value));
1243
+ const hasCommitIntent = (value = '') => /\b(confirm\w*|confim\w*|cnfirm\w*|cofnirm\w*|ocnfi\w*|ocnfia\w*|fechar|aprovar|finalizar|prosseguir|continuar|sim)\b/.test(normalizeIntentText(value));
1272
1244
  const isConversationRecallQuery = (value = '') => {
1273
1245
  const text = normalizeIntentText(value);
1274
1246
  return /\b(minha|qual|quais|o que|oq|voce|vc|lembra|lembrar|sabe|diga|fala)\b.{0,80}\b(ultima|ultimas|msg|msgs|mensagem|mensagens|pergunta|perguntei|falei|disse)\b/.test(text)
1275
1247
  || /\b(o que|oq)\b.{0,30}\b(eu)\b.{0,30}\b(falei|disse|perguntei|mandei)\b/.test(text)
1276
1248
  || /\b(nao|nao)\b.{0,20}\b(lembra|sabe)\b.{0,60}\b(msg|msgs|mensagem|pergunta|falei|disse|perguntei)\b/.test(text);
1277
1249
  };
1278
- const isAgendaStatusQuery = (value = '') => {
1250
+ const isOperationalStatusQuery = (value = '') => {
1279
1251
  const text = normalizeIntentText(value);
1280
- return /\b(ja|ainda|tinha|tenho|foi|esta|ta)\b.{0,60}\b(agend\w*|agned\w*|ganed\w*|marc\w*|reserv\w*|confirm\w*)\b/.test(text)
1281
- || /\b(agend\w*|agned\w*|ganed\w*|marc\w*|reserv\w*|confirm\w*)\b.{0,60}\b(ja|ainda|feito|feita|confirmado|confirmada)\b/.test(text);
1252
+ return /\b(quais?|qual|lista|mostra|resume|status|estado|historico|hist[oó]rico|outputs?|retornos?|resultado|resultados?)\b.{0,80}\b(tools?|ferramentas?|apis?|a[cç][oõ]es?|chamadas?|executadas?|usadas?)\b/.test(text)
1253
+ || /\b(tools?|ferramentas?|apis?|a[cç][oõ]es?|chamadas?|executadas?|usadas?)\b.{0,80}\b(quais?|qual|lista|mostra|resume|status|estado|historico|hist[oó]rico|outputs?|retornos?|resultado|resultados?)\b/.test(text)
1254
+ || /\b(ja|ainda|tinha|tenho|foi|esta|ta)\b.{0,60}\b(feito|feita|executad\w*|chamad\w*|concluid\w*|confirm\w*|criad\w*|atualizad\w*|agend\w*|agned\w*|ganed\w*|marc\w*|reserv\w*)\b/.test(text);
1282
1255
  };
1283
1256
  const recentUserMessages = (recentMessages = []) => [...(recentMessages || [])]
1284
1257
  .filter((msg) => /^(user|human)$/i.test(String(msg.role || '')))
@@ -1336,8 +1309,8 @@ const buildConversationFrame = ({ query = '', recentMessages = [], workingMemory
1336
1309
  };
1337
1310
  const buildCurrentTurnFocus = ({ query = '', recentMessages = [], operationalState = {}, workingMemory = {} }) => {
1338
1311
  const recall = isConversationRecallQuery(query);
1339
- const agendaStatus = isAgendaStatusQuery(query);
1340
- if (!recall && !agendaStatus)
1312
+ const operationalStatus = isOperationalStatusQuery(query);
1313
+ if (!recall && !operationalStatus)
1341
1314
  return null;
1342
1315
  const previousUser = previousUserMessageForQuery(query, recentMessages);
1343
1316
  const firstUser = firstUserMessageFromConversation(recentMessages);
@@ -1345,31 +1318,22 @@ const buildCurrentTurnFocus = ({ query = '', recentMessages = [], operationalSta
1345
1318
  .filter((msg) => normalizeIntentText(msg.content).trim() !== normalizeIntentText(query).trim())
1346
1319
  .slice(0, 3)
1347
1320
  .map((msg) => ({ role: msg.role, content: truncate(msg.content, 500), at: msg.at }));
1348
- const agenda = (operationalState || {}).agenda_state || {};
1321
+ const toolState = (operationalState || {}).tool_state || {};
1349
1322
  const previousUserMessage = previousUser ? truncate(previousUser.content, 500) : previousUserFallbackFromWorkingMemory(query, workingMemory);
1350
1323
  const focus = cleanContextValue({
1351
1324
  current_user_request: truncate(query, 500),
1352
- intent: recall ? 'conversation_recall' : 'agenda_status_question',
1325
+ intent: recall ? 'conversation_recall' : 'tool_or_status_question',
1353
1326
  first_user_message: firstUser ? truncate(firstUser.content, 500) : null,
1354
1327
  previous_user_message: previousUserMessage,
1355
1328
  recent_user_messages: users,
1356
- agenda_status: agendaStatus ? {
1357
- reservation_status: agenda.has_confirmation && !agenda.has_pending_pre_reservation
1358
- ? 'confirmed'
1359
- : agenda.has_pending_pre_reservation
1360
- ? 'pending_pre_reservation'
1361
- : agenda.has_pre_reservation
1362
- ? 'pre_reserved'
1363
- : agenda.has_availability
1364
- ? 'availability_known_not_scheduled'
1365
- : 'none',
1366
- has_confirmation: Boolean(agenda.has_confirmation && !agenda.has_pending_pre_reservation),
1367
- has_pending_pre_reservation: Boolean(agenda.has_pending_pre_reservation),
1368
- has_availability: Boolean(agenda.has_availability),
1329
+ tool_status: operationalStatus ? {
1330
+ executed_tools: toolState.names || [],
1331
+ last_successful_tool: toolState.last_successful_tool || null,
1332
+ failed_by_name: toolState.failed_by_name || {},
1369
1333
  } : undefined,
1370
1334
  instruction: recall
1371
- ? 'Answer the user meta-question from previous_user_message/recent_user_messages and the chronological transcript. Do not answer with agenda availability unless the user asks for availability.'
1372
- : 'Answer whether the appointment/reservation is already scheduled using agenda_status. Do not offer availability as the main answer unless there is no schedule/reservation state.',
1335
+ ? 'Answer the user meta-question from previous_user_message/recent_user_messages and the chronological transcript. Do not call tools for recall-only questions.'
1336
+ : 'Answer status questions from tool_state, tool_history and action_ledger. Do not call tools unless the agent prompt requires it.',
1373
1337
  });
1374
1338
  if (!previousUserMessage)
1375
1339
  focus.previous_user_message = null;
@@ -1377,47 +1341,29 @@ const buildCurrentTurnFocus = ({ query = '', recentMessages = [], operationalSta
1377
1341
  };
1378
1342
  const buildActionDirective = ({ workingMemory = {}, operationalState = {} }) => {
1379
1343
  const next = String((workingMemory || {}).next_expected_action || '');
1380
- const match = /\bagenda_(consultar_disponibilidade|pre_reservar_horario|confirmar_agendamento)\b/.exec(next);
1381
- if (!match || !/\bcall\b|\bMUST\b/i.test(next))
1344
+ const tool = pickRequiredToolFromAction(next);
1345
+ if (!tool || !/\bcall\b|\bMUST\b/i.test(next))
1382
1346
  return null;
1383
- const tool = match[0];
1384
- const agenda = (operationalState || {}).agenda_state || {};
1347
+ const toolState = (operationalState || {}).tool_state || {};
1348
+ const latestForTool = ((toolState.latest_by_name || {})[tool]) || null;
1385
1349
  return cleanContextValue({
1386
1350
  required_tool: tool,
1387
1351
  next_expected_action: next,
1388
- agenda_state: {
1389
- has_availability: Boolean(agenda.has_availability),
1390
- latest_availability_source: agenda.latest_availability_source,
1391
- has_pre_reservation: Boolean(agenda.has_pre_reservation),
1392
- has_pending_pre_reservation: Boolean(agenda.has_pending_pre_reservation),
1393
- has_confirmation: Boolean(agenda.has_confirmation),
1352
+ tool_state: {
1353
+ last_tool: (operationalState || {}).last_tool || null,
1354
+ required_tool_last_result: latestForTool,
1355
+ counts_by_name: toolState.counts_by_name || {},
1356
+ failed_by_name: toolState.failed_by_name || {},
1394
1357
  },
1395
- instruction: `You MUST call ${tool} now. Do not answer with "posso", "se quiser", "já posso" or ask for the same confirmation instead of calling the required tool. Only skip this tool if it is explicitly blocked in Tool guard.`,
1358
+ 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.`,
1396
1359
  });
1397
1360
  };
1398
1361
  const inferToolGuard = ({ query, recentMessages, toolHistory, vectorMemories }) => {
1399
- const text = normalizeIntentText(query);
1400
- const names = new Set((toolHistory || []).map((tool) => String(tool.name || '')));
1401
- const hasRecentContext = (recentMessages || []).some((msg) => /reserva|pré-reserva|pre-reserva|agend|confirm|hor[aá]rio|dispon/i.test(String(msg.content || '')));
1402
- const hasVectorContext = (vectorMemories || []).some((memory) => /reserva|pré-reserva|pre-reserva|agend|confirm|hor[aá]rio|dispon/i.test(memoryText(memory)));
1403
- const hasAvailabilityTool = names.has('agenda_consultar_disponibilidade');
1404
- const hasPreReservationTool = names.has('agenda_pre_reservar_horario');
1405
- const hasAnyContext = names.size > 0 || hasRecentContext || hasVectorContext;
1406
- const blocked = [];
1407
- const reasons = [];
1408
- if (!hasAnyContext) {
1409
- blocked.push('agenda_pre_reservar_horario', 'agenda_confirmar_agendamento');
1410
- reasons.push('no prior availability or pre-reservation context was found for this session');
1411
- }
1412
- if (hasConfirmIntent(text) && !hasPreReservationTool) {
1413
- blocked.push('agenda_confirmar_agendamento');
1414
- reasons.push('confirmation requested but no structured agenda_pre_reservar_horario tool result was found; assistant text or vector memories are not enough to confirm');
1415
- }
1416
- if ((/pre.?reserv|pre.?reserv|reservar/.test(text)) && !hasAvailabilityTool) {
1417
- blocked.push('agenda_pre_reservar_horario');
1418
- reasons.push('reservation requested but no structured agenda_consultar_disponibilidade tool result was found');
1419
- }
1420
- return { blockedTools: Array.from(new Set(blocked)), reasons };
1362
+ return {
1363
+ blockedTools: [],
1364
+ reasons: [],
1365
+ instruction: 'Memory does not block domain tools by hardcoded rules. The agent prompt decides which tool to call; use tool_state/tool_history/action_ledger to verify prior inputs, outputs and failures.',
1366
+ };
1421
1367
  };
1422
1368
  const inferUserIntent = (query = '', recentMessages = []) => {
1423
1369
  const latestUser = [...(recentMessages || [])].reverse().find((msg) => /^(user|human)$/i.test(String(msg.role || '')));
@@ -1426,20 +1372,14 @@ const inferUserIntent = (query = '', recentMessages = []) => {
1426
1372
  const normalizedQuery = normalizeIntentText(query).trim();
1427
1373
  if (isConversationRecallQuery(query))
1428
1374
  return 'conversation_recall';
1429
- if (isAgendaStatusQuery(query))
1430
- return 'agenda_status_question';
1375
+ if (isOperationalStatusQuery(query))
1376
+ return 'operational_status_question';
1431
1377
  if (/^(ok|sim|pode|pode sim|isso|isso mesmo|confirmo|confirmar)$/.test(normalizedQuery))
1432
1378
  return 'affirm';
1433
- if (hasConfirmIntent(text))
1434
- return 'confirm';
1435
- if (containsSelectedSlot(text))
1436
- return 'select_slot';
1437
- if (/\b(pre.?reserv|pre.?reserv|reservar|segurar|marcar)\b/.test(text))
1438
- return 'pre_reserve';
1439
- if (/\b(disponibilidade|horarios?|agenda(?:r|mento)?|quando pode|tem vaga|quero agendar|gostaria de agendar|preciso agendar)\b/.test(text))
1440
- return 'check_availability';
1441
- if (/\b(cancel|desmarcar|remarcar|alterar|mudar)\b/.test(text))
1442
- return 'change_or_cancel';
1379
+ if (hasCommitIntent(text))
1380
+ return 'commit_or_continue';
1381
+ if (/\b(buscar|busca|criar|cria|atualizar|atualiza|consultar|consulta|reservar|reserva|agendar|agenda|abrir|abre|cancelar|cancela|enviar|envia|gerar|gera|validar|valida|processar|processa|executar|executa)\b/.test(text))
1382
+ return 'tool_action_candidate';
1443
1383
  if (/\b(meu nome|email|telefone|empresa|prefiro|preferencia)\b/.test(text))
1444
1384
  return 'profile_update';
1445
1385
  if (text.trim())
@@ -1447,41 +1387,14 @@ const inferUserIntent = (query = '', recentMessages = []) => {
1447
1387
  return 'unknown';
1448
1388
  };
1449
1389
  const deriveNextExpectedAction = (intent, operationalState = {}) => {
1450
- const agenda = operationalState.agenda_state || {};
1451
- if (intent === 'affirm') {
1452
- if (agenda.has_pending_pre_reservation)
1453
- return 'MUST call agenda_confirmar_agendamento now; do not ask the same confirmation question again';
1454
- if (agenda.has_availability)
1455
- return 'MUST call agenda_pre_reservar_horario now using the selected or latest discussed slot; do not ask again';
1456
- return 'call agenda_consultar_disponibilidade before reserving';
1457
- }
1458
- if (intent === 'confirm') {
1459
- if (agenda.has_pending_pre_reservation)
1460
- return 'MUST call agenda_confirmar_agendamento now using the latest pre-reservation context; do not ask again';
1461
- if (agenda.has_availability)
1462
- return 'MUST call agenda_pre_reservar_horario first using the selected or latest discussed slot; do not ask for permission again';
1463
- return 'do not call agenda_confirmar_agendamento; call agenda_consultar_disponibilidade first because no availability is known';
1464
- }
1465
- if (intent === 'select_slot') {
1466
- if (agenda.has_availability)
1467
- return 'MUST call agenda_pre_reservar_horario now using the selected slot; do not ask for confirmation before pre-reserving';
1468
- return 'call agenda_consultar_disponibilidade before pre-reserving';
1469
- }
1470
- if (intent === 'pre_reserve') {
1471
- if (agenda.has_availability)
1472
- return 'MUST call agenda_pre_reservar_horario now using one of the known available slots';
1473
- return 'call agenda_consultar_disponibilidade before pre-reserving';
1474
- }
1475
- if (intent === 'check_availability')
1476
- return agenda.has_availability ? 'reuse known availability unless the user asks to refresh' : 'call agenda_consultar_disponibilidade';
1477
- if (intent === 'change_or_cancel')
1478
- return 'inspect current reservation state before changing or cancelling';
1390
+ if (['affirm', 'commit_or_continue', 'tool_action_candidate'].includes(intent))
1391
+ return 'continue according to the agent prompt using conversation_frame, tool_state, tool_history and action_ledger; do not apply domain-specific memory rules';
1479
1392
  if (intent === 'profile_update')
1480
1393
  return 'save stable profile facts and continue the conversation';
1481
1394
  if (intent === 'conversation_recall')
1482
- return 'answer directly using recent_messages.previous_user_message; do not call agenda tools';
1483
- if (intent === 'agenda_status_question')
1484
- return 'answer whether there is a confirmed or pending reservation using operational_state; do not consult availability unless status is unknown';
1395
+ return 'answer directly using previous_user_message and conversation_history_chronological; do not call tools for recall-only questions';
1396
+ if (intent === 'operational_status_question')
1397
+ return 'answer status questions from tool_state, tool_history and action_ledger; do not call tools unless the agent prompt requires it';
1485
1398
  return 'answer using retrieved context and avoid unnecessary tool calls';
1486
1399
  };
1487
1400
  const deriveWorkingMemory = ({ query = '', profileFacts = {}, recentMessages = [], toolHistory = [], operationalState = {}, previous = {} }) => {
@@ -1506,11 +1419,11 @@ const deriveWorkingMemory = ({ query = '', profileFacts = {}, recentMessages = [
1506
1419
  };
1507
1420
  };
1508
1421
  const deriveDecisionState = ({ query = '', toolHistory = [], operationalState = {}, workingMemory = {} }) => {
1509
- const agenda = operationalState.agenda_state || {};
1510
1422
  const decisions = [];
1511
1423
  const doNotRepeatTools = [];
1512
1424
  const intent = workingMemory.last_user_intent || inferUserIntent(query, []);
1513
1425
  const now = nowIso();
1426
+ const toolState = operationalState.tool_state || {};
1514
1427
  const pushDecision = (id, decision, reason, appliesTo, extra = {}) => {
1515
1428
  decisions.push({
1516
1429
  id,
@@ -1523,41 +1436,25 @@ const deriveDecisionState = ({ query = '', toolHistory = [], operationalState =
1523
1436
  created_at: extra.created_at || now,
1524
1437
  valid_until: extra.valid_until || null,
1525
1438
  superseded_by: extra.superseded_by || null,
1439
+ tool: extra.tool,
1526
1440
  });
1527
1441
  };
1528
- if (agenda.has_availability) {
1529
- doNotRepeatTools.push('agenda_consultar_disponibilidade');
1530
- pushDecision('agenda_availability_known', 'do not consult availability again unless the user asks to refresh', 'availability result already exists in tool_history', 'agenda_flow', { confidence: 0.9 });
1531
- }
1532
- if (agenda.has_pending_pre_reservation) {
1533
- doNotRepeatTools.push('agenda_pre_reservar_horario');
1534
- pushDecision('agenda_pre_reservation_pending', 'confirmation should use the latest pre-reservation context', 'a pre-reservation exists after the latest confirmation', 'agenda_flow', { confidence: 0.92 });
1535
- }
1536
- if (agenda.has_confirmation && !agenda.has_pending_pre_reservation) {
1537
- doNotRepeatTools.push('agenda_confirmar_agendamento');
1538
- pushDecision('agenda_confirmation_done', 'do not confirm again unless the user explicitly asks to repeat or change it', 'latest reservation appears confirmed', 'agenda_flow', { confidence: 0.9 });
1539
- }
1540
- const reservationStatus = agenda.has_confirmation && !agenda.has_pending_pre_reservation
1541
- ? 'confirmed'
1542
- : agenda.has_pending_pre_reservation
1543
- ? 'pending_pre_reservation'
1544
- : agenda.has_pre_reservation
1545
- ? 'pre_reserved'
1546
- : agenda.has_availability
1547
- ? 'availability_known'
1548
- : 'none';
1442
+ for (const name of Object.keys(toolState.counts_by_name || {})) {
1443
+ doNotRepeatTools.push(name);
1444
+ }
1445
+ if (toolState.last_successful_tool)
1446
+ pushDecision('last_successful_tool_recorded', `latest successful tool is ${toolState.last_successful_tool.name}`, 'tool_state records successful tool execution', 'tool_orchestration', { confidence: 0.9, tool: toolState.last_successful_tool.name });
1447
+ for (const [name, count] of Object.entries(toolState.failed_by_name || {}))
1448
+ pushDecision(`tool_failed_${name}`, `${name} failed ${count} time(s); inspect tool output before retrying`, 'tool_state records failed tool execution', 'tool_orchestration', { confidence: 0.86, tool: name });
1549
1449
  return {
1550
1450
  active_decisions: decisions,
1551
1451
  do_not_repeat_tools: Array.from(new Set(doNotRepeatTools)),
1552
1452
  current_intent: intent,
1553
- agenda_decision_state: {
1554
- pending_pre_reservation: Boolean(agenda.has_pending_pre_reservation),
1555
- active_reservation: Boolean(agenda.has_pre_reservation || agenda.has_confirmation),
1556
- confirmed_reservation: Boolean(agenda.has_confirmation && !agenda.has_pending_pre_reservation),
1557
- superseded_reservation: Boolean(agenda.has_confirmation && agenda.has_pending_pre_reservation),
1558
- reschedule_requested: intent === 'change_or_cancel' && Boolean(agenda.has_confirmation || agenda.has_pre_reservation),
1559
- cancel_requested: /\b(cancelar|cancele|desmarcar|remover)\b/i.test(String(query || '')),
1560
- reservation_status: reservationStatus,
1453
+ tool_decision_state: {
1454
+ executed_tools: Object.keys(toolState.counts_by_name || {}),
1455
+ failed_tools: Object.keys(toolState.failed_by_name || {}),
1456
+ last_successful_tool: toolState.last_successful_tool || null,
1457
+ do_not_repeat_tools: Array.from(new Set(doNotRepeatTools)),
1561
1458
  evaluated_at: now,
1562
1459
  },
1563
1460
  conflict_policy: 'prefer active decisions with newer timestamps; ignore superseded or expired memories unless auditing',
@@ -1608,7 +1505,12 @@ const deriveMemoryCompression = ({ recentMessages = [], toolHistory = [], profil
1608
1505
  session_summary: {
1609
1506
  messages: lastMessages.length,
1610
1507
  tools: lastTools,
1611
- agenda_state: operationalState.agenda_state || {},
1508
+ tool_state: (operationalState || {}).tool_state ? {
1509
+ names: ((operationalState.tool_state || {}).names || []).slice(0, maxItems),
1510
+ counts_by_name: (operationalState.tool_state || {}).counts_by_name || {},
1511
+ failed_by_name: (operationalState.tool_state || {}).failed_by_name || {},
1512
+ last_successful_tool: (operationalState.tool_state || {}).last_successful_tool || null,
1513
+ } : {},
1612
1514
  },
1613
1515
  entity_summary: profile,
1614
1516
  workflow_summary: {
@@ -1654,7 +1556,6 @@ const deriveContextHealth = ({ userId = '', project = '', vectorMemories = [], r
1654
1556
  score += weights[key] || 0;
1655
1557
  }
1656
1558
  const missing = Object.entries(checks).filter(([, ok]) => !ok).map(([key]) => key);
1657
- const agenda = operationalState.agenda_state || {};
1658
1559
  return {
1659
1560
  kind: 'tembory.context_health.v1',
1660
1561
  namespace: userId || null,
@@ -1669,11 +1570,10 @@ const deriveContextHealth = ({ userId = '', project = '', vectorMemories = [], r
1669
1570
  tool_history: Array.isArray(toolHistory) ? toolHistory.length : 0,
1670
1571
  active_decisions: Array.isArray(decisionState.active_decisions) ? decisionState.active_decisions.length : 0,
1671
1572
  },
1672
- agenda_state: {
1673
- has_availability: Boolean(agenda.has_availability),
1674
- has_pre_reservation: Boolean(agenda.has_pre_reservation),
1675
- has_confirmation: Boolean(agenda.has_confirmation),
1676
- has_pending_pre_reservation: Boolean(agenda.has_pending_pre_reservation),
1573
+ tool_state: {
1574
+ names: (((operationalState || {}).tool_state || {}).names || []).slice(0, 12),
1575
+ last_successful_tool: (((operationalState || {}).tool_state || {}).last_successful_tool) || null,
1576
+ failed_by_name: (((operationalState || {}).tool_state || {}).failed_by_name) || {},
1677
1577
  },
1678
1578
  generated_at: nowIso(),
1679
1579
  };
@@ -1684,13 +1584,13 @@ const contextMemoryText = (memory, max = 700) => {
1684
1584
  if (tools.length) {
1685
1585
  const names = tools.map((tool) => `${tool.name || 'tool'}:${tool.ok === false ? 'failed' : 'ok'}`).join(', ');
1686
1586
  const ids = tools.map((tool) => tool.result || tool.input || '').join(' ');
1687
- const idMatch = /(reservation_id|confirmation_id|id)["':\s]+([A-Za-z0-9_.:-]+)/i.exec(ids);
1587
+ const idMatch = /([A-Za-z_][A-Za-z0-9_.:-]*id)["':\s]+([A-Za-z0-9_.:-]+)/i.exec(ids);
1688
1588
  return truncate(`[tool_events_extracted] ${names}${idMatch ? ` ${idMatch[1]}=${idMatch[2]}` : ''}. See Tool history for structured details.`, max);
1689
1589
  }
1690
1590
  return truncate(text, max);
1691
1591
  };
1692
1592
  const approxTokenCount = (text) => Math.ceil(String(text || '').length / 4);
1693
- const importantJsonFields = ['reservation_id', 'confirmation_id', 'selected_from_message', 'status', 'next_step', 'available_slots', 'timezone', 'error'];
1593
+ 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'];
1694
1594
  const pickImportantFields = (value) => {
1695
1595
  if (value === null || value === undefined)
1696
1596
  return {};
@@ -1720,9 +1620,7 @@ const compactToolResult = (result, max = 360) => {
1720
1620
  return undefined;
1721
1621
  try {
1722
1622
  const parsed = JSON.parse(text);
1723
- const picked = pickImportantFields(parsed);
1724
- if (Object.keys(picked).length)
1725
- return truncate(safeStringify(picked), max);
1623
+ return truncate(safeStringify(parsed), max);
1726
1624
  }
1727
1625
  catch { }
1728
1626
  const picked = {};
@@ -1741,8 +1639,9 @@ const compactToolHistoryForAgent = (toolHistory = [], maxItems = 6, includeResul
1741
1639
  name: tool.name,
1742
1640
  status: tool.ok === false ? 'failed' : 'ok',
1743
1641
  at: tool.at,
1744
- input: truncate(String(tool.input || ''), 180) || undefined,
1745
- result: includeResults ? compactToolResult(tool.result, 360) : undefined,
1642
+ reason: truncate(String(tool.reason || tool.decision || tool.why || tool.source || 'recorded tool call'), 140),
1643
+ input: truncate(String(tool.input || ''), 140) || undefined,
1644
+ result: includeResults ? compactToolResult(tool.result, 240) : undefined,
1746
1645
  }));
1747
1646
  const cleanContextValue = (value) => {
1748
1647
  if (Array.isArray(value)) {
@@ -1786,7 +1685,7 @@ const compactDecisionStateForAgent = (state = {}) => cleanContextValue({
1786
1685
  at: decision.at || decision.updated_at,
1787
1686
  })),
1788
1687
  do_not_repeat_tools: state.do_not_repeat_tools,
1789
- agenda_decision_state: state.agenda_decision_state,
1688
+ tool_decision_state: state.tool_decision_state,
1790
1689
  latest_tool: state.latest_tool ? cleanContextValue({
1791
1690
  name: state.latest_tool.name,
1792
1691
  status: state.latest_tool.status || (state.latest_tool.ok === false ? 'failed' : 'ok'),
@@ -1795,19 +1694,21 @@ const compactDecisionStateForAgent = (state = {}) => cleanContextValue({
1795
1694
  conflict_policy: state.conflict_policy,
1796
1695
  });
1797
1696
  const compactOperationalStateForAgent = (state = {}) => {
1798
- const agenda = state.agenda_state || {};
1697
+ const counts = state.tool_counts || {};
1698
+ const toolState = state.tool_state || {};
1799
1699
  return {
1800
1700
  last_tool: state.last_tool || null,
1801
- tool_counts: state.tool_counts || {},
1802
- agenda_state: {
1803
- has_availability: Boolean(agenda.has_availability),
1804
- has_pre_reservation: Boolean(agenda.has_pre_reservation),
1805
- has_confirmation: Boolean(agenda.has_confirmation),
1806
- has_pending_pre_reservation: Boolean(agenda.has_pending_pre_reservation),
1807
- latest_availability: compactToolResult(agenda.latest_availability_result),
1808
- latest_pre_reservation: compactToolResult(agenda.latest_pre_reservation_result),
1809
- latest_confirmation: compactToolResult(agenda.latest_confirmation_result),
1701
+ tool_counts: {
1702
+ total: counts.total || 0,
1703
+ ok: counts.ok || 0,
1704
+ failed: counts.failed || 0,
1810
1705
  },
1706
+ tool_state: (toolState.names || []).length ? {
1707
+ names: toolState.names,
1708
+ counts_by_name: toolState.counts_by_name || {},
1709
+ failed_by_name: toolState.failed_by_name || {},
1710
+ last_successful_tool: toolState.last_successful_tool || undefined,
1711
+ } : undefined,
1811
1712
  blocked_without_context: state.blocked_without_context || [],
1812
1713
  guidance: (state.guidance || []).slice(0, 3),
1813
1714
  };
@@ -1815,12 +1716,6 @@ const compactOperationalStateForAgent = (state = {}) => {
1815
1716
  const compactMemoryCompressionForAgent = (compression = {}) => ({
1816
1717
  turn_summary: (compression.turn_summary || []).slice(-3),
1817
1718
  session_tools: (((compression.session_summary || {}).tools) || []).slice(-6),
1818
- agenda_state: ((compression.session_summary || {}).agenda_state) ? {
1819
- has_availability: Boolean(compression.session_summary.agenda_state.has_availability),
1820
- has_pre_reservation: Boolean(compression.session_summary.agenda_state.has_pre_reservation),
1821
- has_confirmation: Boolean(compression.session_summary.agenda_state.has_confirmation),
1822
- has_pending_pre_reservation: Boolean(compression.session_summary.agenda_state.has_pending_pre_reservation),
1823
- } : undefined,
1824
1719
  active_memory_count: ((compression.workflow_summary || {}).active_memory_count) || 0,
1825
1720
  });
1826
1721
  const compactActionLedgerForAgent = (ledger = [], maxItems = 6, includeResults = true) => pruneByLimit(ledger || [], maxItems).map((item) => cleanContextValue({
@@ -1829,7 +1724,9 @@ const compactActionLedgerForAgent = (ledger = [], maxItems = 6, includeResults =
1829
1724
  tool: item.tool || item.name,
1830
1725
  status: item.status,
1831
1726
  at: item.at,
1832
- result: includeResults ? compactToolResult(item.result, 260) : undefined,
1727
+ reason: item.reason,
1728
+ input: item.input,
1729
+ result: includeResults ? compactToolResult(item.result, 180) : undefined,
1833
1730
  }));
1834
1731
  const compactEntityTimelineForAgent = (timeline = [], maxItems = 8) => pruneByLimit(timeline || [], maxItems).map((item) => cleanContextValue({
1835
1732
  entity: item.entity || item.source || item.name,
@@ -2047,8 +1944,8 @@ const buildContextMessages = ({ payloadFormat, query, userId, profileFacts, work
2047
1944
  section: 'context_header',
2048
1945
  title: 'Tembory context',
2049
1946
  value: compactForAgent || compactStateSections
2050
- ? 'Read-only memory. Conversation frame is authoritative for the full recent transcript, including first/current/previous client messages. Follow next_expected_action when present. Before calling downstream tools, verify required prior tool context in tool_history or operational_state. Do not repeat tools listed in do_not_repeat_tools.'
2051
- : 'Use this context as read-only memory. Prefer it over guessing. Do not mention internal section names to the user. The Conversation frame is authoritative for the full recent transcript, including first, current, previous, and recent client messages. Treat next_expected_action as an instruction, not as a suggestion. If it says to call a tool now, call that tool instead of asking the user the same question again. If the user asks to continue, chooses a slot, says ok/sim, reserve, confirm, update, cancel, or performs any downstream action that depends on a prior tool result, first verify the required prior result in tool_history, recent_messages, or vector memories. If the required prior result is absent, do not call the downstream tool; ask for the missing context or call the appropriate prerequisite tool.',
1947
+ ? 'Read-only operational memory. Conversation frame is authoritative for user/assistant transcript. Tool history/action ledger are authoritative for tool calls, reasons, inputs, outputs, status and timestamps. The agent prompt owns domain policy and decides which tool to call.'
1948
+ : 'Use this context as read-only operational memory. Do not mention internal section names to the user. Conversation frame is authoritative for the chronological user/assistant transcript. Tool history and action ledger are authoritative for what tools were called, why they were called, what input was sent, what output returned, whether they failed, and when they ran. Memory must not invent domain-specific tool rules; the agent prompt owns tool orchestration policy.',
2052
1949
  });
2053
1950
  }
2054
1951
  sections.push({
@@ -2184,9 +2081,9 @@ const buildContextMessages = ({ payloadFormat, query, userId, profileFacts, work
2184
2081
  value: {
2185
2082
  blocked_tools: guard.blockedTools,
2186
2083
  reasons: guard.reasons,
2187
- instruction: guard.blockedTools.length
2084
+ instruction: guard.instruction || (guard.blockedTools.length
2188
2085
  ? `Do not call these tools now: ${guard.blockedTools.join(', ')}. Ask for the missing context or execute the prerequisite step first.`
2189
- : 'No downstream tool is blocked by missing memory prerequisites.',
2086
+ : 'No downstream tool is blocked by missing memory prerequisites.'),
2190
2087
  },
2191
2088
  });
2192
2089
  sections.push({
@@ -3712,8 +3609,6 @@ exports.__private = {
3712
3609
  explicitToolHistoryItemsFromMemory,
3713
3610
  toolHistoryFromMemory,
3714
3611
  cleanAssistantTranscriptText,
3715
- availabilityFromRecentMessages,
3716
- preReservationFromRecentMessages,
3717
3612
  buildActionDirective,
3718
3613
  recentMessageFromMemory,
3719
3614
  previousUserFallbackFromWorkingMemory,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "n8n-nodes-tembory",
3
- "version": "1.0.31",
3
+ "version": "1.0.32",
4
4
  "description": "Tembory node for n8n AI Agents with profile, tools, timeline, graph and semantic memory",
5
5
  "license": "MIT",
6
6
  "homepage": "https://tembory.com",
@@ -25,7 +25,8 @@
25
25
  "test": "node --test test/*.test.js",
26
26
  "smoke:n8n:multiturn": "node scripts/smoke-n8n-multiturn-tools.js",
27
27
  "simulate:agent-context": "node scripts/simulate-agent-context.js",
28
- "prepublishOnly": "npm test"
28
+ "simulate:n8n-agent": "node scripts/simulate-n8n-agent-agenda.js",
29
+ "prepublishOnly": "npm test && npm run simulate:n8n-agent"
29
30
  },
30
31
  "files": [
31
32
  "dist",
@@ -27,53 +27,49 @@ const core = require('../dist/nodes/Mem0/Mem0Memory.node.js').__private;
27
27
 
28
28
  const toolHistory = [
29
29
  {
30
- id: 'tool_ry3twg',
31
- name: 'agenda_consultar_disponibilidade',
32
- input: '',
30
+ id: 'tool_crm_lookup',
31
+ name: 'crm_buscar_cliente',
32
+ input: '{"phone":"11999998888"}',
33
33
  ok: true,
34
34
  at: '2026-05-14T05:34:12.460Z',
35
- source: 'used_tools_text',
36
- result: '[{"args":{"tool":"agenda_consultar_disponibilidade","available_slots":"2026-05-12T09:00,2026-05-12T14:00,2026-05-13T10:30","timezone":"America/Sao_Paulo","next_step":"escolher_um_horario"}}]',
35
+ source: 'structured_tool_event',
36
+ reason: 'agent prompt requested CRM lookup by phone',
37
+ result: '{"customer_id":"CUS-TEMBORY-LAB-001","name":"Lucas","phone":"11999998888"}',
37
38
  },
38
39
  {
39
- id: 'tool_mu9qd2',
40
- name: 'agenda_pre_reservar_horario',
41
- input: '',
40
+ id: 'tool_charge',
41
+ name: 'pagamento_criar_cobranca',
42
+ input: '{"customer_id":"CUS-TEMBORY-LAB-001","amount_cents":9700}',
42
43
  ok: true,
43
44
  at: '2026-05-14T05:34:34.017Z',
44
- source: 'used_tools_text',
45
- result: '[{"args":{"tool":"agenda_pre_reservar_horario","reservation_id":"RES-TEMBORY-LAB-001","selected_from_message":"dia 13","status":"pre_reserved","next_step":"confirmar_agendamento"}}]',
45
+ source: 'structured_tool_event',
46
+ reason: 'agent prompt requested payment creation',
47
+ result: '{"charge_id":"CHG-TEMBORY-LAB-001","customer_id":"CUS-TEMBORY-LAB-001","status":"created"}',
46
48
  },
47
49
  ];
48
50
 
49
51
  const vectorMemories = [
50
52
  {
51
- memory: '[Used tools: Tool: agenda_pre_reservar_horario, Input: , Result: [{"args":{"tool":"agenda_pre_reservar_horario","reservation_id":"RES-TEMBORY-LAB-001","selected_from_message":"dia 13","status":"pre_reserved","next_step":"confirmar_agendamento"}}]] Calling agenda_pre_reservar_horario with input: {"id":"call_bfHlltcjI1vCagRkav0fj8Ha"}\nPré-reserva feita para o dia 13.\nSe quiser, posso confirmar agora.',
53
+ memory: '[Used tools: Tool: pagamento_criar_cobranca, Input: {"customer_id":"CUS-TEMBORY-LAB-001"}, Result: {"charge_id":"CHG-TEMBORY-LAB-001","status":"created"}] Cobrança criada para Lucas.',
52
54
  created_at: '2026-05-14T05:34:35.373020Z',
53
55
  temboryScore: { semanticScore: 0.71, recencyScore: 0.99, hybridScore: 0.81, source: 'semantic' },
54
56
  },
55
- {
56
- memory: '[Used tools: Tool: agenda_consultar_disponibilidade, Input: , Result: [{"args":{"tool":"agenda_consultar_disponibilidade","available_slots":"2026-05-12T09:00,2026-05-12T14:00,2026-05-13T10:30","timezone":"America/Sao_Paulo","next_step":"escolher_um_horario"}}]] Temos estes horários vagos:\n- 12/05 às 09:00\n- 12/05 às 14:00\n- 13/05 às 10:30',
57
- created_at: '2026-05-14T05:34:14.717988Z',
58
- temboryScore: { semanticScore: 0.7, recencyScore: 0.99, hybridScore: 0.8, source: 'semantic' },
59
- },
60
- { memory: 'Oi! Como posso ajudar?', created_at: '2026-05-14T05:33:47.194630Z' },
61
- { memory: 'oi', created_at: '2026-05-14T05:33:47.194355Z' },
57
+ { memory: 'Lucas prefere atendimento financeiro por WhatsApp.', created_at: '2026-05-14T05:33:47.194630Z' },
62
58
  ];
63
59
 
64
60
  const operationalState = core.deriveOperationalState(toolHistory, {}, [], true);
65
61
  const workingMemory = {
66
- current_goal: 'call agenda_confirmar_agendamento now; do not ask the same confirmation question again',
67
- current_task: 'call agenda_confirmar_agendamento now; do not ask the same confirmation question again',
68
- last_user_intent: 'affirm',
69
- last_user_message: 'ok',
70
- next_expected_action: 'call agenda_confirmar_agendamento now; do not ask the same confirmation question again',
62
+ current_goal: 'call suporte_abrir_ticket now using prior CRM/payment context',
63
+ current_task: 'call suporte_abrir_ticket now using prior CRM/payment context',
64
+ last_user_intent: 'tool_action_candidate',
65
+ last_user_message: 'abre um ticket com financeiro',
66
+ next_expected_action: 'call suporte_abrir_ticket now using prior CRM/payment context',
71
67
  };
72
- const decisionState = core.deriveDecisionState({ query: 'ok', toolHistory, operationalState, workingMemory });
68
+ const decisionState = core.deriveDecisionState({ query: 'abre um ticket com financeiro', toolHistory, operationalState, workingMemory });
73
69
  const memoryCompression = core.deriveMemoryCompression({ toolHistory, operationalState, vectorMemories });
74
70
  const common = {
75
71
  payloadFormat: 'structured',
76
- query: 'ok',
72
+ query: 'abre um ticket com financeiro',
77
73
  userId: 'd48292bd4c9f4eccb9120092e3acd073',
78
74
  profileFacts: {},
79
75
  workingMemory,
@@ -86,8 +82,8 @@ const common = {
86
82
  recentMessages: [],
87
83
  toolHistory,
88
84
  highlights: [
89
- 'tool agenda_consultar_disponibilidade: [{"args":{"available_slots":"2026-05-12T09:00,2026-05-12T14:00,2026-05-13T10:30"}}]',
90
- 'tool agenda_pre_reservar_horario: [{"args":{"reservation_id":"RES-TEMBORY-LAB-001","status":"pre_reserved"}}]',
85
+ 'tool crm_buscar_cliente: {"customer_id":"CUS-TEMBORY-LAB-001"}',
86
+ 'tool pagamento_criar_cobranca: {"charge_id":"CHG-TEMBORY-LAB-001"}',
91
87
  ],
92
88
  graph: [],
93
89
  diagnostics: {},
@@ -106,8 +102,8 @@ for (const [name, adv] of modes) {
106
102
  size: core.contextSizeOfMessages(messages),
107
103
  has_next_expected_action: /next_expected_action/.test(text),
108
104
  has_do_not_repeat_tools: /do_not_repeat_tools/.test(text),
109
- has_reservation_id: /RES-TEMBORY-LAB-001/.test(text),
110
- has_available_slots: /available_slots/.test(text),
111
- has_duplicate_calling_text: /Calling agenda_pre_reservar_horario/.test(text),
105
+ has_customer_id: /CUS-TEMBORY-LAB-001/.test(text),
106
+ has_charge_id: /CHG-TEMBORY-LAB-001/.test(text),
107
+ has_duplicate_calling_text: /Calling\s+[A-Za-z_][A-Za-z0-9_.:-]*/.test(text),
112
108
  }, null, 2));
113
109
  }
@@ -0,0 +1,327 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const assert = require('node:assert/strict');
5
+ const Module = require('node:module');
6
+
7
+ const originalLoad = Module._load;
8
+ Module._load = function patchedLoad(request, parent, isMain) {
9
+ if (request === 'n8n-workflow') {
10
+ return {
11
+ NodeConnectionTypes: {
12
+ AiMemory: 'ai_memory',
13
+ AiLanguageModel: 'ai_languageModel',
14
+ AiEmbedding: 'ai_embedding',
15
+ },
16
+ NodeApiError: class NodeApiError extends Error {},
17
+ };
18
+ }
19
+ if (request === '@langchain/core/messages') {
20
+ return {
21
+ HumanMessage: class HumanMessage { constructor(content) { this.content = content; } _getType() { return 'human'; } },
22
+ AIMessage: class AIMessage { constructor(content) { this.content = content; } _getType() { return 'ai'; } },
23
+ SystemMessage: class SystemMessage { constructor(content) { this.content = content; } _getType() { return 'system'; } },
24
+ };
25
+ }
26
+ return originalLoad.call(this, request, parent, isMain);
27
+ };
28
+
29
+ const core = require('../dist/nodes/Mem0/Mem0Memory.node.js').__private;
30
+
31
+ const BASE_TIME = Date.parse('2026-05-16T12:00:00.000Z');
32
+
33
+ function at(index) {
34
+ return new Date(BASE_TIME + index * 1000).toISOString();
35
+ }
36
+
37
+ function parseContext(messages) {
38
+ return messages.map((message) => message.content || '').join('\n\n');
39
+ }
40
+
41
+ function requiredToolFromText(text) {
42
+ return /"required_tool":"([^"]+)"/.exec(text)?.[1] || null;
43
+ }
44
+
45
+ function previousUserMessageFromText(text) {
46
+ return /"previous_user_message":"((?:\\"|[^"])*)"/.exec(text)?.[1]?.replace(/\\"/g, '"') || '';
47
+ }
48
+
49
+ function normalizeText(value) {
50
+ return String(value || '').toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, '');
51
+ }
52
+
53
+ function mockTool(name, userMessage) {
54
+ const outputs = {
55
+ crm_buscar_cliente: {
56
+ customer_id: 'CUS-TEMBORY-LAB-001',
57
+ name: 'Lucas',
58
+ phone: '11999998888',
59
+ lifecycle_stage: 'lead_qualified',
60
+ },
61
+ crm_atualizar_pipeline: {
62
+ customer_id: 'CUS-TEMBORY-LAB-001',
63
+ pipeline_id: 'PIPE-VENDAS-001',
64
+ stage: 'proposal',
65
+ updated_from_message: userMessage,
66
+ },
67
+ pagamento_criar_cobranca: {
68
+ charge_id: 'CHG-TEMBORY-LAB-001',
69
+ customer_id: 'CUS-TEMBORY-LAB-001',
70
+ amount_cents: 9700,
71
+ status: 'created',
72
+ },
73
+ suporte_abrir_ticket: {
74
+ ticket_id: 'TCK-TEMBORY-LAB-001',
75
+ customer_id: 'CUS-TEMBORY-LAB-001',
76
+ queue: 'financeiro',
77
+ status: 'opened',
78
+ },
79
+ api_enviar_webhook: {
80
+ webhook_id: 'WH-TEMBORY-LAB-001',
81
+ endpoint: 'https://example.test/hooks/customer',
82
+ status: 'delivered',
83
+ },
84
+ };
85
+ if (!outputs[name]) throw new Error(`Unknown mock tool: ${name}`);
86
+ return outputs[name];
87
+ }
88
+
89
+ function answerFor({ userMessage, toolCalls, state, contextText }) {
90
+ const normalized = normalizeText(userMessage);
91
+ if (/o que eu ja te disse|o que eu já te disse/.test(normalized)) {
92
+ const facts = core.renderProfileFacts(state.profileFacts);
93
+ return [
94
+ 'Você me disse que:',
95
+ facts.name ? `- seu nome é ${facts.name}` : null,
96
+ facts.phone ? `- seu telefone é ${facts.phone}` : null,
97
+ facts.company ? `- sua empresa é ${facts.company}` : null,
98
+ Array.isArray(facts.interests) && facts.interests.length ? `- interesses: ${facts.interests.join(', ')}` : null,
99
+ ].filter(Boolean).join('\n');
100
+ }
101
+ if (/quais tools|tools foram|ferramentas foram|outputs/.test(normalized)) {
102
+ if (!state.toolHistory.length) return 'Nenhuma tool foi chamada ainda.';
103
+ return `Foram chamadas ${state.toolHistory.length} tools:\n` + state.toolHistory
104
+ .map((tool, index) => `${index + 1}. ${tool.name}: ${tool.result}`)
105
+ .join('\n');
106
+ }
107
+ if (/qual.*ultima mensagem antes/.test(normalized)) {
108
+ const previous = previousUserMessageFromText(contextText)
109
+ || core.previousUserFallbackFromWorkingMemory(userMessage, state.workingMemory);
110
+ return previous ? `Sua última mensagem antes desta foi: "${previous}".` : 'Não encontrei mensagem anterior.';
111
+ }
112
+ if (toolCalls.length) {
113
+ return `Tool executada: ${toolCalls.map((tool) => tool.name).join(', ')}.`;
114
+ }
115
+ return userMessage.includes('lucas') ? 'Prazer, Lucas! Como posso ajudar?' : 'Perfeito.';
116
+ }
117
+
118
+ function buildTurnContext({ state, userMessage, visibleToolHistory, turnIndex, requiredTool = null }) {
119
+ state.recentMessages.push({ role: 'user', content: userMessage, at: at(turnIndex * 2) });
120
+ state.profileFacts = core.mergeProfileFacts(
121
+ state.profileFacts,
122
+ core.extractProfileFactsFromText(userMessage, 'user_message', at(turnIndex * 2)),
123
+ core.mergeProfileFacts(...state.recentMessages
124
+ .filter((message) => /^(user|human)$/i.test(String(message.role || '')))
125
+ .map((message) => core.extractProfileFactsFromText(message.content, 'recent_user_message', message.at))),
126
+ );
127
+
128
+ const renderedProfile = core.renderProfileFacts(state.profileFacts);
129
+ const operationalState = core.deriveOperationalState(visibleToolHistory, renderedProfile, state.recentMessages, true);
130
+ const workingMemory = core.deriveWorkingMemory({
131
+ query: userMessage,
132
+ profileFacts: renderedProfile,
133
+ recentMessages: state.recentMessages,
134
+ toolHistory: visibleToolHistory,
135
+ operationalState,
136
+ previous: state.workingMemory,
137
+ });
138
+ if (requiredTool) {
139
+ workingMemory.current_task = `MUST call ${requiredTool} now because the agent prompt selected this tool`;
140
+ workingMemory.next_expected_action = `MUST call ${requiredTool} now because the agent prompt selected this tool`;
141
+ workingMemory.last_user_intent = 'tool_action_candidate';
142
+ }
143
+ const decisionState = core.deriveDecisionState({
144
+ query: userMessage,
145
+ toolHistory: visibleToolHistory,
146
+ operationalState,
147
+ workingMemory,
148
+ });
149
+ const adv = core.applyOperationalPreset({ operationPreset: 'productionBalanced' });
150
+ const contextMessages = core.buildContextMessages({
151
+ payloadFormat: 'structured',
152
+ query: userMessage,
153
+ userId: state.namespace,
154
+ profileFacts: renderedProfile,
155
+ workingMemory,
156
+ decisionState,
157
+ memoryCompression: core.deriveMemoryCompression({
158
+ recentMessages: state.recentMessages,
159
+ toolHistory: visibleToolHistory,
160
+ profileFacts: state.profileFacts,
161
+ operationalState,
162
+ vectorMemories: [],
163
+ }),
164
+ operationalState,
165
+ actionLedger: core.deriveActionLedger(visibleToolHistory, 20, true),
166
+ entityTimeline: core.deriveEntityTimeline(state.profileFacts, [], state.recentMessages, visibleToolHistory),
167
+ vectorMemories: [],
168
+ recentMessages: state.recentMessages,
169
+ toolHistory: visibleToolHistory,
170
+ highlights: [],
171
+ graph: [],
172
+ diagnostics: {},
173
+ adv,
174
+ });
175
+
176
+ state.workingMemory = workingMemory;
177
+ state.decisionState = decisionState;
178
+ state.operationalState = operationalState;
179
+
180
+ return { contextText: parseContext(contextMessages), workingMemory, operationalState };
181
+ }
182
+
183
+ function runScenario({ name, turns, expectedCounts, hideToolHistoryOnTurns = new Set() }) {
184
+ const state = {
185
+ namespace: `local-${name}`,
186
+ recentMessages: [],
187
+ toolHistory: [],
188
+ profileFacts: {},
189
+ workingMemory: {},
190
+ decisionState: {},
191
+ operationalState: {},
192
+ };
193
+ const trace = [];
194
+
195
+ for (let index = 0; index < turns.length; index += 1) {
196
+ const turn = turns[index];
197
+ const turnNumber = index + 1;
198
+ const visibleToolHistory = hideToolHistoryOnTurns.has(turnNumber) ? [] : state.toolHistory;
199
+ const { contextText, workingMemory, operationalState } = buildTurnContext({
200
+ state,
201
+ userMessage: turn.user,
202
+ visibleToolHistory,
203
+ turnIndex: index,
204
+ requiredTool: turn.tool,
205
+ });
206
+
207
+ assert.doesNotMatch(contextText, /\[Used tools:|Calling\s+[A-Za-z_][A-Za-z0-9_.:-]*/i, `${name} turn ${turnNumber}: raw tool audit leaked into context`);
208
+
209
+ const toolName = requiredToolFromText(contextText);
210
+ const toolCalls = [];
211
+ if (toolName) {
212
+ const result = mockTool(toolName, turn.user);
213
+ const extracted = core.extractToolCalls({
214
+ output: '',
215
+ __temboryToolCalls: [{
216
+ id: `${name}-turn-${turnNumber}-${toolName}`,
217
+ sequence: 1,
218
+ name: toolName,
219
+ input: JSON.stringify({ user_message: turn.user }),
220
+ result: JSON.stringify([{ args: { tool: toolName, ...result } }]),
221
+ ok: true,
222
+ at: at(index * 2 + 1),
223
+ source: 'local_n8n_agent_simulator',
224
+ reason: `agent_prompt_selected_${toolName}`,
225
+ }],
226
+ });
227
+ state.toolHistory = core.applyToolHistoryWindow(state.toolHistory.concat(extracted), 3600, 20);
228
+ toolCalls.push(...extracted);
229
+ }
230
+
231
+ const answer = core.cleanAssistantTranscriptText(answerFor({ userMessage: turn.user, toolCalls, state, contextText }));
232
+ state.recentMessages.push({ role: 'assistant', content: answer, at: at(index * 2 + 1) });
233
+ state.recentMessages = core.pruneConversationMessagesPreserveAnchors(state.recentMessages, 30);
234
+
235
+ trace.push({
236
+ turn: turnNumber,
237
+ user: turn.user,
238
+ requiredTool: toolName,
239
+ calledTools: toolCalls.map((tool) => tool.name),
240
+ toolState: operationalState.tool_state,
241
+ nextExpected: workingMemory.next_expected_action,
242
+ answer,
243
+ });
244
+ }
245
+
246
+ for (const [toolName, expectedCount] of Object.entries(expectedCounts)) {
247
+ assert.equal(state.toolHistory.filter((tool) => tool.name === toolName).length, expectedCount, `${name}: unexpected count for ${toolName}`);
248
+ }
249
+ assert.match(trace.at(-1).answer, /quais tools foram chamadas/i, `${name}: previous message recall failed`);
250
+
251
+ const finalOperational = core.deriveOperationalState(state.toolHistory, core.renderProfileFacts(state.profileFacts), state.recentMessages, true);
252
+ const health = core.deriveContextHealth({
253
+ userId: state.namespace,
254
+ project: 'local-agent-simulation',
255
+ vectorMemories: [{ memory: 'local simulation' }],
256
+ recentMessages: state.recentMessages,
257
+ toolHistory: state.toolHistory,
258
+ workingMemory: state.workingMemory,
259
+ decisionState: state.decisionState,
260
+ memoryCompression: core.deriveMemoryCompression({
261
+ recentMessages: state.recentMessages,
262
+ toolHistory: state.toolHistory,
263
+ profileFacts: state.profileFacts,
264
+ operationalState: finalOperational,
265
+ vectorMemories: [],
266
+ }),
267
+ operationalState: finalOperational,
268
+ diagnostics: { connectedAi: { languageModel: true, embedding: true, errors: [] } },
269
+ });
270
+
271
+ assert.ok(health.quality_score >= 970, `${name}: health score too low: ${health.quality_score}`);
272
+
273
+ return {
274
+ name,
275
+ ok: true,
276
+ toolHistory: state.toolHistory.map((tool) => tool.name),
277
+ profileFacts: core.renderProfileFacts(state.profileFacts),
278
+ qualityScore: health.quality_score,
279
+ status: health.status,
280
+ trace,
281
+ };
282
+ }
283
+
284
+ const results = [
285
+ runScenario({
286
+ name: 'generic-crm-payment-support-flow',
287
+ turns: [
288
+ { user: 'oi meu nome eh lucas' },
289
+ { user: 'meu telefone eh 11999998888' },
290
+ { user: 'minha empresa chama Elefai' },
291
+ { user: 'busca meu cadastro pelo telefone', tool: 'crm_buscar_cliente' },
292
+ { user: 'atualiza meu pipeline para proposta', tool: 'crm_atualizar_pipeline' },
293
+ { user: 'cria uma cobrança para mim', tool: 'pagamento_criar_cobranca' },
294
+ { user: 'abre um ticket com o financeiro', tool: 'suporte_abrir_ticket' },
295
+ { user: 'envia webhook para meu sistema', tool: 'api_enviar_webhook' },
296
+ { user: 'quais tools foram chamadas e quais outputs elas deram?' },
297
+ { user: 'qual foi minha ultima mensagem antes desta?' },
298
+ ],
299
+ expectedCounts: {
300
+ crm_buscar_cliente: 1,
301
+ crm_atualizar_pipeline: 1,
302
+ pagamento_criar_cobranca: 1,
303
+ suporte_abrir_ticket: 1,
304
+ api_enviar_webhook: 1,
305
+ },
306
+ }),
307
+ runScenario({
308
+ name: 'retrieval-gap-still-uses-agent-selected-tools',
309
+ hideToolHistoryOnTurns: new Set([5, 6]),
310
+ turns: [
311
+ { user: 'oi meu nome eh lucas' },
312
+ { user: 'busca meu cadastro pelo telefone', tool: 'crm_buscar_cliente' },
313
+ { user: 'quais tools foram chamadas?' },
314
+ { user: 'cria uma cobrança para mim', tool: 'pagamento_criar_cobranca' },
315
+ { user: 'abre ticket mesmo se o histórico estruturado não vier agora', tool: 'suporte_abrir_ticket' },
316
+ { user: 'quais tools foram chamadas e quais outputs elas deram?' },
317
+ { user: 'qual foi minha ultima mensagem antes desta?' },
318
+ ],
319
+ expectedCounts: {
320
+ crm_buscar_cliente: 1,
321
+ pagamento_criar_cobranca: 1,
322
+ suporte_abrir_ticket: 1,
323
+ },
324
+ }),
325
+ ];
326
+
327
+ console.log(JSON.stringify({ ok: true, scenarios: results }, null, 2));
@@ -30,31 +30,31 @@ const core = require('../dist/nodes/Mem0/Mem0Memory.node.js').__private;
30
30
 
31
31
  const turns = [
32
32
  {
33
- user: 'Quais horarios existem para uma reuniao esta semana?',
34
- tools: [{ name: 'agenda_consultar_disponibilidade', input: { range: 'week' }, result: { slots: ['2026-05-15T14:00:00-03:00'] } }],
33
+ user: 'Busca meu cadastro no CRM pelo telefone',
34
+ tools: [{ name: 'crm_buscar_cliente', input: { phone: '11999998888' }, result: { customer_id: 'CUS-TEMBORY-001' } }],
35
35
  },
36
- { user: 'Pode usar o horario das 14h', tools: [] },
36
+ { user: 'Pode usar esse cadastro', tools: [] },
37
37
  {
38
- user: 'Faz a pre-reserva desse horario',
39
- tools: [{ name: 'agenda_pre_reservar_horario', input: { slot: '2026-05-15T14:00:00-03:00' }, result: { reservation_id: 'RES-TEMBORY-001' } }],
38
+ user: 'Atualiza o pipeline para proposta',
39
+ tools: [{ name: 'crm_atualizar_pipeline', input: { customer_id: 'CUS-TEMBORY-001', stage: 'proposal' }, result: { pipeline_id: 'PIPE-TEMBORY-001', stage: 'proposal' } }],
40
40
  },
41
- { user: 'Qual pre-reserva ficou pendente?', tools: [] },
41
+ { user: 'Qual atualização ficou registrada?', tools: [] },
42
42
  {
43
- user: 'Pode confirmar',
44
- tools: [{ name: 'agenda_confirmar_agendamento', input: { reservation_id: 'RES-TEMBORY-001' }, result: { confirmation_id: 'CONF-TEMBORY-001' } }],
43
+ user: 'Cria uma cobrança para esse cliente',
44
+ tools: [{ name: 'pagamento_criar_cobranca', input: { customer_id: 'CUS-TEMBORY-001' }, result: { charge_id: 'CHG-TEMBORY-001' } }],
45
45
  },
46
- { user: 'Qual foi o codigo de confirmacao?', tools: [] },
46
+ { user: 'Qual foi o codigo da cobrança?', tools: [] },
47
47
  {
48
- user: 'Atualiza disponibilidade para outro horario',
49
- tools: [{ name: 'agenda_consultar_disponibilidade', input: { refresh: true }, result: { slots: ['2026-05-16T10:00:00-03:00'] } }],
48
+ user: 'Abre ticket para o financeiro',
49
+ tools: [{ name: 'suporte_abrir_ticket', input: { customer_id: 'CUS-TEMBORY-001', queue: 'financeiro' }, result: { ticket_id: 'TCK-TEMBORY-001' } }],
50
50
  },
51
51
  {
52
- user: 'Pre-reserva o novo horario',
53
- tools: [{ name: 'agenda_pre_reservar_horario', input: { slot: '2026-05-16T10:00:00-03:00' }, result: { reservation_id: 'RES-TEMBORY-002' } }],
52
+ user: 'Envia webhook para meu sistema',
53
+ tools: [{ name: 'api_enviar_webhook', input: { customer_id: 'CUS-TEMBORY-001' }, result: { webhook_id: 'WH-TEMBORY-001', status: 'delivered' } }],
54
54
  },
55
55
  {
56
- user: 'Confirma o novo horario',
57
- tools: [{ name: 'agenda_confirmar_agendamento', input: { reservation_id: 'RES-TEMBORY-002' }, result: { confirmation_id: 'CONF-TEMBORY-002' } }],
56
+ user: 'Atualiza o CRM com o webhook enviado',
57
+ tools: [{ name: 'crm_registrar_evento', input: { customer_id: 'CUS-TEMBORY-001', event: 'webhook_delivered' }, result: { event_id: 'EVT-TEMBORY-001' } }],
58
58
  },
59
59
  { user: 'Resume as tools usadas e o estado atual', tools: [] },
60
60
  { user: 'O que voce sabe sem chamar ferramenta?', tools: [] },
@@ -103,14 +103,14 @@ for (const [index, turn] of turns.entries()) {
103
103
  toolHistory,
104
104
  profileFacts: {},
105
105
  operationalState,
106
- vectorMemories: [{ memory: 'CONF-TEMBORY-002 confirmado' }],
106
+ vectorMemories: [{ memory: 'EVT-TEMBORY-001 registrado' }],
107
107
  });
108
108
  }
109
109
 
110
110
  const health = core.deriveContextHealth({
111
111
  userId: namespace,
112
112
  project: 'smoke',
113
- vectorMemories: [{ memory: 'CONF-TEMBORY-002 confirmado' }],
113
+ vectorMemories: [{ memory: 'EVT-TEMBORY-001 registrado' }],
114
114
  recentMessages,
115
115
  toolHistory,
116
116
  workingMemory,
@@ -122,9 +122,10 @@ const health = core.deriveContextHealth({
122
122
 
123
123
  assert.equal(recentMessages.length, 12);
124
124
  assert.equal(toolHistory.length, 6);
125
- assert.equal(operationalState.agenda_state.has_confirmation, true);
126
- assert.equal(decisionState.agenda_decision_state.confirmed_reservation, true);
127
- assert.ok(decisionState.do_not_repeat_tools.includes('agenda_confirmar_agendamento'));
125
+ assert.equal(operationalState.tool_state.counts_by_name.crm_buscar_cliente, 1);
126
+ assert.equal(operationalState.tool_state.counts_by_name.crm_registrar_evento, 1);
127
+ assert.equal(decisionState.tool_decision_state.last_successful_tool.name, 'crm_registrar_evento');
128
+ assert.ok(decisionState.do_not_repeat_tools.includes('crm_registrar_evento'));
128
129
  assert.ok(health.quality_score >= 970);
129
130
 
130
131
  console.log(JSON.stringify({
@@ -134,5 +135,5 @@ console.log(JSON.stringify({
134
135
  toolEvents: toolHistory.length,
135
136
  qualityScore: health.quality_score,
136
137
  status: health.status,
137
- latestConfirmation: operationalState.agenda_state.latest_confirmation_result,
138
+ latestTool: operationalState.tool_state.last_successful_tool,
138
139
  }, null, 2));