n8n-nodes-tembory 1.0.30 → 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.
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.0.30`.
5
+ Versao atual: `1.0.31`.
6
+
7
+ ## 1.0.31
8
+
9
+ - Considera pre-reserva recente no transcript como estado operacional valido quando `tool_history` ainda nao foi recuperado no turno.
10
+ - Evita chamar `agenda_pre_reservar_horario` novamente antes de confirmar quando a mensagem recente ja diz que houve pre-reserva.
11
+ - Direciona confirmacoes como "pode confirmar" direto para `agenda_confirmar_agendamento` quando existe pre-reserva recente pendente.
6
12
 
7
13
  ## 1.0.30
8
14
 
@@ -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,53 +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
- };
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 '';
429
425
  };
430
426
  const deriveOperationalState = (toolHistory = [], profileFacts = {}, recentMessages = [], includeResults = true) => {
431
427
  const tools = Array.isArray(toolHistory) ? toolHistory : [];
432
428
  const successfulTools = tools.filter((tool) => tool.ok !== false);
433
- const byName = (name) => successfulTools.filter((tool) => tool.name === name);
434
- const availability = byName('agenda_consultar_disponibilidade');
435
- const recentAvailability = availability.length ? null : availabilityFromRecentMessages(recentMessages);
436
- const hasAvailability = availability.length > 0 || Boolean(recentAvailability);
437
- const reservations = byName('agenda_pre_reservar_horario');
438
- 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
+ }
439
462
  const lastTool = tools[tools.length - 1] || null;
440
- const lastReservation = reservations[reservations.length - 1] || null;
441
- const lastConfirmation = confirmations[confirmations.length - 1] || null;
442
- const hasPendingReservation = Boolean(lastReservation && (!lastConfirmation || String(lastReservation.at || '') > String(lastConfirmation.at || '')));
443
463
  const blockedWithoutContext = [];
444
- if (!hasAvailability)
445
- blockedWithoutContext.push('agenda_pre_reservar_horario');
446
- if (!reservations.length)
447
- blockedWithoutContext.push('agenda_confirmar_agendamento');
448
- const guidance = [];
449
- if (!hasAvailability)
450
- guidance.push('No availability result is known for this session; consult availability before reserving or confirming.');
451
- else if (hasPendingReservation)
452
- guidance.push('A pre-reservation exists after the latest confirmation; confirmation can use the latest pre-reservation context.');
453
- else if (lastConfirmation)
454
- guidance.push('The latest reservation appears confirmed; do not confirm again unless the user explicitly asks to repeat or change it.');
455
- else if (hasAvailability)
456
- 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
+ ];
457
467
  return {
458
468
  profile_complete: Boolean(profileFacts && profileFacts.name && profileFacts.company && profileFacts.email && profileFacts.phone),
459
469
  last_tool: lastTool ? { name: lastTool.name, ok: lastTool.ok, at: lastTool.at } : null,
@@ -461,19 +471,22 @@ const deriveOperationalState = (toolHistory = [], profileFacts = {}, recentMessa
461
471
  total: tools.length,
462
472
  ok: successfulTools.length,
463
473
  failed: tools.length - successfulTools.length,
464
- agenda_consultar_disponibilidade: availability.length,
465
- agenda_pre_reservar_horario: reservations.length,
466
- agenda_confirmar_agendamento: confirmations.length,
474
+ by_name: toolCountsByName,
467
475
  },
468
- agenda_state: {
469
- has_availability: hasAvailability,
470
- has_pre_reservation: reservations.length > 0,
471
- has_confirmation: confirmations.length > 0,
472
- has_pending_pre_reservation: hasPendingReservation,
473
- latest_availability_result: availability.length ? maybeToolResult(availability[availability.length - 1], includeResults) : (recentAvailability && includeResults !== false ? recentAvailability.text : null),
474
- latest_availability_source: availability.length ? 'tool_history' : (recentAvailability ? recentAvailability.source : null),
475
- latest_pre_reservation_result: lastReservation ? maybeToolResult(lastReservation, includeResults) : null,
476
- 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,
477
490
  },
478
491
  blocked_without_context: Array.from(new Set(blockedWithoutContext)),
479
492
  guidance,
@@ -488,6 +501,7 @@ const deriveActionLedger = (toolHistory = [], maxItems = 20, includeResults = tr
488
501
  kind: 'tool_call',
489
502
  name: tool.name || 'unknown_tool',
490
503
  status: tool.ok === false ? 'failed' : 'ok',
504
+ reason: truncate(String(tool.reason || tool.decision || tool.why || tool.source || 'recorded tool call'), 260),
491
505
  input: compactToolPayload(safeParseToolPayload(tool.input)),
492
506
  result: maybeToolResult(tool, includeResults),
493
507
  at: tool.at || null,
@@ -664,7 +678,7 @@ const toolHistoryFromMemory = (item) => {
664
678
  source: 'semantic_fact',
665
679
  };
666
680
  }
667
- 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;
668
682
  if (namedTool) {
669
683
  return {
670
684
  id: meta.id || meta.call_id || meta.callId || '',
@@ -677,26 +691,6 @@ const toolHistoryFromMemory = (item) => {
677
691
  source: 'semantic_named_tool',
678
692
  };
679
693
  }
680
- const text = meta.kind === 'tool_history' ? String(content || '').toLowerCase() : '';
681
- const inferredAgendaTool = text.includes('confirm') && text.includes('agendamento')
682
- ? 'agenda_confirmar_agendamento'
683
- : text.includes('pré-reserva') || text.includes('pre-reserva') || text.includes('pre reserva') || text.includes('pré reserva')
684
- ? 'agenda_pre_reservar_horario'
685
- : (text.includes('horários disponíveis') || text.includes('horarios disponiveis') || text.includes('available') || text.includes('disponibilidade')) && text.includes('agenda')
686
- ? 'agenda_consultar_disponibilidade'
687
- : '';
688
- if (inferredAgendaTool) {
689
- return {
690
- id: meta.id || meta.call_id || meta.callId || '',
691
- turnId: meta.turn_id || meta.turnId || '',
692
- name: inferredAgendaTool,
693
- input: '',
694
- ok: true,
695
- result: truncate(content || '', 1000),
696
- at: meta.at || item.created_at || item.createdAt || nowIso(),
697
- source: 'semantic_inference',
698
- };
699
- }
700
694
  if (meta.kind !== 'tool_history')
701
695
  return null;
702
696
  const name = meta.name || meta.tool || meta.toolName;
@@ -1246,17 +1240,18 @@ const normalizeIntentText = (value = '') => String(value || '')
1246
1240
  .toLowerCase()
1247
1241
  .normalize('NFD')
1248
1242
  .replace(/[\u0300-\u036f]/g, '');
1249
- 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));
1250
1244
  const isConversationRecallQuery = (value = '') => {
1251
1245
  const text = normalizeIntentText(value);
1252
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)
1253
1247
  || /\b(o que|oq)\b.{0,30}\b(eu)\b.{0,30}\b(falei|disse|perguntei|mandei)\b/.test(text)
1254
1248
  || /\b(nao|nao)\b.{0,20}\b(lembra|sabe)\b.{0,60}\b(msg|msgs|mensagem|pergunta|falei|disse|perguntei)\b/.test(text);
1255
1249
  };
1256
- const isAgendaStatusQuery = (value = '') => {
1250
+ const isOperationalStatusQuery = (value = '') => {
1257
1251
  const text = normalizeIntentText(value);
1258
- 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)
1259
- || /\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);
1260
1255
  };
1261
1256
  const recentUserMessages = (recentMessages = []) => [...(recentMessages || [])]
1262
1257
  .filter((msg) => /^(user|human)$/i.test(String(msg.role || '')))
@@ -1314,8 +1309,8 @@ const buildConversationFrame = ({ query = '', recentMessages = [], workingMemory
1314
1309
  };
1315
1310
  const buildCurrentTurnFocus = ({ query = '', recentMessages = [], operationalState = {}, workingMemory = {} }) => {
1316
1311
  const recall = isConversationRecallQuery(query);
1317
- const agendaStatus = isAgendaStatusQuery(query);
1318
- if (!recall && !agendaStatus)
1312
+ const operationalStatus = isOperationalStatusQuery(query);
1313
+ if (!recall && !operationalStatus)
1319
1314
  return null;
1320
1315
  const previousUser = previousUserMessageForQuery(query, recentMessages);
1321
1316
  const firstUser = firstUserMessageFromConversation(recentMessages);
@@ -1323,31 +1318,22 @@ const buildCurrentTurnFocus = ({ query = '', recentMessages = [], operationalSta
1323
1318
  .filter((msg) => normalizeIntentText(msg.content).trim() !== normalizeIntentText(query).trim())
1324
1319
  .slice(0, 3)
1325
1320
  .map((msg) => ({ role: msg.role, content: truncate(msg.content, 500), at: msg.at }));
1326
- const agenda = (operationalState || {}).agenda_state || {};
1321
+ const toolState = (operationalState || {}).tool_state || {};
1327
1322
  const previousUserMessage = previousUser ? truncate(previousUser.content, 500) : previousUserFallbackFromWorkingMemory(query, workingMemory);
1328
1323
  const focus = cleanContextValue({
1329
1324
  current_user_request: truncate(query, 500),
1330
- intent: recall ? 'conversation_recall' : 'agenda_status_question',
1325
+ intent: recall ? 'conversation_recall' : 'tool_or_status_question',
1331
1326
  first_user_message: firstUser ? truncate(firstUser.content, 500) : null,
1332
1327
  previous_user_message: previousUserMessage,
1333
1328
  recent_user_messages: users,
1334
- agenda_status: agendaStatus ? {
1335
- reservation_status: agenda.has_confirmation && !agenda.has_pending_pre_reservation
1336
- ? 'confirmed'
1337
- : agenda.has_pending_pre_reservation
1338
- ? 'pending_pre_reservation'
1339
- : agenda.has_pre_reservation
1340
- ? 'pre_reserved'
1341
- : agenda.has_availability
1342
- ? 'availability_known_not_scheduled'
1343
- : 'none',
1344
- has_confirmation: Boolean(agenda.has_confirmation && !agenda.has_pending_pre_reservation),
1345
- has_pending_pre_reservation: Boolean(agenda.has_pending_pre_reservation),
1346
- 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 || {},
1347
1333
  } : undefined,
1348
1334
  instruction: recall
1349
- ? '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.'
1350
- : '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.',
1351
1337
  });
1352
1338
  if (!previousUserMessage)
1353
1339
  focus.previous_user_message = null;
@@ -1355,47 +1341,29 @@ const buildCurrentTurnFocus = ({ query = '', recentMessages = [], operationalSta
1355
1341
  };
1356
1342
  const buildActionDirective = ({ workingMemory = {}, operationalState = {} }) => {
1357
1343
  const next = String((workingMemory || {}).next_expected_action || '');
1358
- const match = /\bagenda_(consultar_disponibilidade|pre_reservar_horario|confirmar_agendamento)\b/.exec(next);
1359
- if (!match || !/\bcall\b|\bMUST\b/i.test(next))
1344
+ const tool = pickRequiredToolFromAction(next);
1345
+ if (!tool || !/\bcall\b|\bMUST\b/i.test(next))
1360
1346
  return null;
1361
- const tool = match[0];
1362
- const agenda = (operationalState || {}).agenda_state || {};
1347
+ const toolState = (operationalState || {}).tool_state || {};
1348
+ const latestForTool = ((toolState.latest_by_name || {})[tool]) || null;
1363
1349
  return cleanContextValue({
1364
1350
  required_tool: tool,
1365
1351
  next_expected_action: next,
1366
- agenda_state: {
1367
- has_availability: Boolean(agenda.has_availability),
1368
- latest_availability_source: agenda.latest_availability_source,
1369
- has_pre_reservation: Boolean(agenda.has_pre_reservation),
1370
- has_pending_pre_reservation: Boolean(agenda.has_pending_pre_reservation),
1371
- 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 || {},
1372
1357
  },
1373
- 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.`,
1374
1359
  });
1375
1360
  };
1376
1361
  const inferToolGuard = ({ query, recentMessages, toolHistory, vectorMemories }) => {
1377
- const text = normalizeIntentText(query);
1378
- const names = new Set((toolHistory || []).map((tool) => String(tool.name || '')));
1379
- const hasRecentContext = (recentMessages || []).some((msg) => /reserva|pré-reserva|pre-reserva|agend|confirm|hor[aá]rio|dispon/i.test(String(msg.content || '')));
1380
- const hasVectorContext = (vectorMemories || []).some((memory) => /reserva|pré-reserva|pre-reserva|agend|confirm|hor[aá]rio|dispon/i.test(memoryText(memory)));
1381
- const hasAvailabilityTool = names.has('agenda_consultar_disponibilidade');
1382
- const hasPreReservationTool = names.has('agenda_pre_reservar_horario');
1383
- const hasAnyContext = names.size > 0 || hasRecentContext || hasVectorContext;
1384
- const blocked = [];
1385
- const reasons = [];
1386
- if (!hasAnyContext) {
1387
- blocked.push('agenda_pre_reservar_horario', 'agenda_confirmar_agendamento');
1388
- reasons.push('no prior availability or pre-reservation context was found for this session');
1389
- }
1390
- if (hasConfirmIntent(text) && !hasPreReservationTool) {
1391
- blocked.push('agenda_confirmar_agendamento');
1392
- 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');
1393
- }
1394
- if ((/pre.?reserv|pre.?reserv|reservar/.test(text)) && !hasAvailabilityTool) {
1395
- blocked.push('agenda_pre_reservar_horario');
1396
- reasons.push('reservation requested but no structured agenda_consultar_disponibilidade tool result was found');
1397
- }
1398
- 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
+ };
1399
1367
  };
1400
1368
  const inferUserIntent = (query = '', recentMessages = []) => {
1401
1369
  const latestUser = [...(recentMessages || [])].reverse().find((msg) => /^(user|human)$/i.test(String(msg.role || '')));
@@ -1404,20 +1372,14 @@ const inferUserIntent = (query = '', recentMessages = []) => {
1404
1372
  const normalizedQuery = normalizeIntentText(query).trim();
1405
1373
  if (isConversationRecallQuery(query))
1406
1374
  return 'conversation_recall';
1407
- if (isAgendaStatusQuery(query))
1408
- return 'agenda_status_question';
1375
+ if (isOperationalStatusQuery(query))
1376
+ return 'operational_status_question';
1409
1377
  if (/^(ok|sim|pode|pode sim|isso|isso mesmo|confirmo|confirmar)$/.test(normalizedQuery))
1410
1378
  return 'affirm';
1411
- if (hasConfirmIntent(text))
1412
- return 'confirm';
1413
- if (containsSelectedSlot(text))
1414
- return 'select_slot';
1415
- if (/\b(pre.?reserv|pre.?reserv|reservar|segurar|marcar)\b/.test(text))
1416
- return 'pre_reserve';
1417
- if (/\b(disponibilidade|horarios?|agenda(?:r|mento)?|quando pode|tem vaga|quero agendar|gostaria de agendar|preciso agendar)\b/.test(text))
1418
- return 'check_availability';
1419
- if (/\b(cancel|desmarcar|remarcar|alterar|mudar)\b/.test(text))
1420
- 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';
1421
1383
  if (/\b(meu nome|email|telefone|empresa|prefiro|preferencia)\b/.test(text))
1422
1384
  return 'profile_update';
1423
1385
  if (text.trim())
@@ -1425,41 +1387,14 @@ const inferUserIntent = (query = '', recentMessages = []) => {
1425
1387
  return 'unknown';
1426
1388
  };
1427
1389
  const deriveNextExpectedAction = (intent, operationalState = {}) => {
1428
- const agenda = operationalState.agenda_state || {};
1429
- if (intent === 'affirm') {
1430
- if (agenda.has_pending_pre_reservation)
1431
- return 'MUST call agenda_confirmar_agendamento now; do not ask the same confirmation question again';
1432
- if (agenda.has_availability)
1433
- return 'MUST call agenda_pre_reservar_horario now using the selected or latest discussed slot; do not ask again';
1434
- return 'call agenda_consultar_disponibilidade before reserving';
1435
- }
1436
- if (intent === 'confirm') {
1437
- if (agenda.has_pending_pre_reservation)
1438
- return 'MUST call agenda_confirmar_agendamento now using the latest pre-reservation context; do not ask again';
1439
- if (agenda.has_availability)
1440
- return 'MUST call agenda_pre_reservar_horario first using the selected or latest discussed slot; do not ask for permission again';
1441
- return 'do not call agenda_confirmar_agendamento; call agenda_consultar_disponibilidade first because no availability is known';
1442
- }
1443
- if (intent === 'select_slot') {
1444
- if (agenda.has_availability)
1445
- return 'MUST call agenda_pre_reservar_horario now using the selected slot; do not ask for confirmation before pre-reserving';
1446
- return 'call agenda_consultar_disponibilidade before pre-reserving';
1447
- }
1448
- if (intent === 'pre_reserve') {
1449
- if (agenda.has_availability)
1450
- return 'MUST call agenda_pre_reservar_horario now using one of the known available slots';
1451
- return 'call agenda_consultar_disponibilidade before pre-reserving';
1452
- }
1453
- if (intent === 'check_availability')
1454
- return agenda.has_availability ? 'reuse known availability unless the user asks to refresh' : 'call agenda_consultar_disponibilidade';
1455
- if (intent === 'change_or_cancel')
1456
- 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';
1457
1392
  if (intent === 'profile_update')
1458
1393
  return 'save stable profile facts and continue the conversation';
1459
1394
  if (intent === 'conversation_recall')
1460
- return 'answer directly using recent_messages.previous_user_message; do not call agenda tools';
1461
- if (intent === 'agenda_status_question')
1462
- 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';
1463
1398
  return 'answer using retrieved context and avoid unnecessary tool calls';
1464
1399
  };
1465
1400
  const deriveWorkingMemory = ({ query = '', profileFacts = {}, recentMessages = [], toolHistory = [], operationalState = {}, previous = {} }) => {
@@ -1484,11 +1419,11 @@ const deriveWorkingMemory = ({ query = '', profileFacts = {}, recentMessages = [
1484
1419
  };
1485
1420
  };
1486
1421
  const deriveDecisionState = ({ query = '', toolHistory = [], operationalState = {}, workingMemory = {} }) => {
1487
- const agenda = operationalState.agenda_state || {};
1488
1422
  const decisions = [];
1489
1423
  const doNotRepeatTools = [];
1490
1424
  const intent = workingMemory.last_user_intent || inferUserIntent(query, []);
1491
1425
  const now = nowIso();
1426
+ const toolState = operationalState.tool_state || {};
1492
1427
  const pushDecision = (id, decision, reason, appliesTo, extra = {}) => {
1493
1428
  decisions.push({
1494
1429
  id,
@@ -1501,41 +1436,25 @@ const deriveDecisionState = ({ query = '', toolHistory = [], operationalState =
1501
1436
  created_at: extra.created_at || now,
1502
1437
  valid_until: extra.valid_until || null,
1503
1438
  superseded_by: extra.superseded_by || null,
1439
+ tool: extra.tool,
1504
1440
  });
1505
1441
  };
1506
- if (agenda.has_availability) {
1507
- doNotRepeatTools.push('agenda_consultar_disponibilidade');
1508
- 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 });
1509
- }
1510
- if (agenda.has_pending_pre_reservation) {
1511
- doNotRepeatTools.push('agenda_pre_reservar_horario');
1512
- 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 });
1513
- }
1514
- if (agenda.has_confirmation && !agenda.has_pending_pre_reservation) {
1515
- doNotRepeatTools.push('agenda_confirmar_agendamento');
1516
- 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 });
1517
- }
1518
- const reservationStatus = agenda.has_confirmation && !agenda.has_pending_pre_reservation
1519
- ? 'confirmed'
1520
- : agenda.has_pending_pre_reservation
1521
- ? 'pending_pre_reservation'
1522
- : agenda.has_pre_reservation
1523
- ? 'pre_reserved'
1524
- : agenda.has_availability
1525
- ? 'availability_known'
1526
- : '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 });
1527
1449
  return {
1528
1450
  active_decisions: decisions,
1529
1451
  do_not_repeat_tools: Array.from(new Set(doNotRepeatTools)),
1530
1452
  current_intent: intent,
1531
- agenda_decision_state: {
1532
- pending_pre_reservation: Boolean(agenda.has_pending_pre_reservation),
1533
- active_reservation: Boolean(agenda.has_pre_reservation || agenda.has_confirmation),
1534
- confirmed_reservation: Boolean(agenda.has_confirmation && !agenda.has_pending_pre_reservation),
1535
- superseded_reservation: Boolean(agenda.has_confirmation && agenda.has_pending_pre_reservation),
1536
- reschedule_requested: intent === 'change_or_cancel' && Boolean(agenda.has_confirmation || agenda.has_pre_reservation),
1537
- cancel_requested: /\b(cancelar|cancele|desmarcar|remover)\b/i.test(String(query || '')),
1538
- 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)),
1539
1458
  evaluated_at: now,
1540
1459
  },
1541
1460
  conflict_policy: 'prefer active decisions with newer timestamps; ignore superseded or expired memories unless auditing',
@@ -1586,7 +1505,12 @@ const deriveMemoryCompression = ({ recentMessages = [], toolHistory = [], profil
1586
1505
  session_summary: {
1587
1506
  messages: lastMessages.length,
1588
1507
  tools: lastTools,
1589
- 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
+ } : {},
1590
1514
  },
1591
1515
  entity_summary: profile,
1592
1516
  workflow_summary: {
@@ -1632,7 +1556,6 @@ const deriveContextHealth = ({ userId = '', project = '', vectorMemories = [], r
1632
1556
  score += weights[key] || 0;
1633
1557
  }
1634
1558
  const missing = Object.entries(checks).filter(([, ok]) => !ok).map(([key]) => key);
1635
- const agenda = operationalState.agenda_state || {};
1636
1559
  return {
1637
1560
  kind: 'tembory.context_health.v1',
1638
1561
  namespace: userId || null,
@@ -1647,11 +1570,10 @@ const deriveContextHealth = ({ userId = '', project = '', vectorMemories = [], r
1647
1570
  tool_history: Array.isArray(toolHistory) ? toolHistory.length : 0,
1648
1571
  active_decisions: Array.isArray(decisionState.active_decisions) ? decisionState.active_decisions.length : 0,
1649
1572
  },
1650
- agenda_state: {
1651
- has_availability: Boolean(agenda.has_availability),
1652
- has_pre_reservation: Boolean(agenda.has_pre_reservation),
1653
- has_confirmation: Boolean(agenda.has_confirmation),
1654
- 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) || {},
1655
1577
  },
1656
1578
  generated_at: nowIso(),
1657
1579
  };
@@ -1662,13 +1584,13 @@ const contextMemoryText = (memory, max = 700) => {
1662
1584
  if (tools.length) {
1663
1585
  const names = tools.map((tool) => `${tool.name || 'tool'}:${tool.ok === false ? 'failed' : 'ok'}`).join(', ');
1664
1586
  const ids = tools.map((tool) => tool.result || tool.input || '').join(' ');
1665
- 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);
1666
1588
  return truncate(`[tool_events_extracted] ${names}${idMatch ? ` ${idMatch[1]}=${idMatch[2]}` : ''}. See Tool history for structured details.`, max);
1667
1589
  }
1668
1590
  return truncate(text, max);
1669
1591
  };
1670
1592
  const approxTokenCount = (text) => Math.ceil(String(text || '').length / 4);
1671
- 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'];
1672
1594
  const pickImportantFields = (value) => {
1673
1595
  if (value === null || value === undefined)
1674
1596
  return {};
@@ -1698,9 +1620,7 @@ const compactToolResult = (result, max = 360) => {
1698
1620
  return undefined;
1699
1621
  try {
1700
1622
  const parsed = JSON.parse(text);
1701
- const picked = pickImportantFields(parsed);
1702
- if (Object.keys(picked).length)
1703
- return truncate(safeStringify(picked), max);
1623
+ return truncate(safeStringify(parsed), max);
1704
1624
  }
1705
1625
  catch { }
1706
1626
  const picked = {};
@@ -1719,8 +1639,9 @@ const compactToolHistoryForAgent = (toolHistory = [], maxItems = 6, includeResul
1719
1639
  name: tool.name,
1720
1640
  status: tool.ok === false ? 'failed' : 'ok',
1721
1641
  at: tool.at,
1722
- input: truncate(String(tool.input || ''), 180) || undefined,
1723
- 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,
1724
1645
  }));
1725
1646
  const cleanContextValue = (value) => {
1726
1647
  if (Array.isArray(value)) {
@@ -1764,7 +1685,7 @@ const compactDecisionStateForAgent = (state = {}) => cleanContextValue({
1764
1685
  at: decision.at || decision.updated_at,
1765
1686
  })),
1766
1687
  do_not_repeat_tools: state.do_not_repeat_tools,
1767
- agenda_decision_state: state.agenda_decision_state,
1688
+ tool_decision_state: state.tool_decision_state,
1768
1689
  latest_tool: state.latest_tool ? cleanContextValue({
1769
1690
  name: state.latest_tool.name,
1770
1691
  status: state.latest_tool.status || (state.latest_tool.ok === false ? 'failed' : 'ok'),
@@ -1773,19 +1694,21 @@ const compactDecisionStateForAgent = (state = {}) => cleanContextValue({
1773
1694
  conflict_policy: state.conflict_policy,
1774
1695
  });
1775
1696
  const compactOperationalStateForAgent = (state = {}) => {
1776
- const agenda = state.agenda_state || {};
1697
+ const counts = state.tool_counts || {};
1698
+ const toolState = state.tool_state || {};
1777
1699
  return {
1778
1700
  last_tool: state.last_tool || null,
1779
- tool_counts: state.tool_counts || {},
1780
- agenda_state: {
1781
- has_availability: Boolean(agenda.has_availability),
1782
- has_pre_reservation: Boolean(agenda.has_pre_reservation),
1783
- has_confirmation: Boolean(agenda.has_confirmation),
1784
- has_pending_pre_reservation: Boolean(agenda.has_pending_pre_reservation),
1785
- latest_availability: compactToolResult(agenda.latest_availability_result),
1786
- latest_pre_reservation: compactToolResult(agenda.latest_pre_reservation_result),
1787
- 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,
1788
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,
1789
1712
  blocked_without_context: state.blocked_without_context || [],
1790
1713
  guidance: (state.guidance || []).slice(0, 3),
1791
1714
  };
@@ -1793,12 +1716,6 @@ const compactOperationalStateForAgent = (state = {}) => {
1793
1716
  const compactMemoryCompressionForAgent = (compression = {}) => ({
1794
1717
  turn_summary: (compression.turn_summary || []).slice(-3),
1795
1718
  session_tools: (((compression.session_summary || {}).tools) || []).slice(-6),
1796
- agenda_state: ((compression.session_summary || {}).agenda_state) ? {
1797
- has_availability: Boolean(compression.session_summary.agenda_state.has_availability),
1798
- has_pre_reservation: Boolean(compression.session_summary.agenda_state.has_pre_reservation),
1799
- has_confirmation: Boolean(compression.session_summary.agenda_state.has_confirmation),
1800
- has_pending_pre_reservation: Boolean(compression.session_summary.agenda_state.has_pending_pre_reservation),
1801
- } : undefined,
1802
1719
  active_memory_count: ((compression.workflow_summary || {}).active_memory_count) || 0,
1803
1720
  });
1804
1721
  const compactActionLedgerForAgent = (ledger = [], maxItems = 6, includeResults = true) => pruneByLimit(ledger || [], maxItems).map((item) => cleanContextValue({
@@ -1807,7 +1724,9 @@ const compactActionLedgerForAgent = (ledger = [], maxItems = 6, includeResults =
1807
1724
  tool: item.tool || item.name,
1808
1725
  status: item.status,
1809
1726
  at: item.at,
1810
- result: includeResults ? compactToolResult(item.result, 260) : undefined,
1727
+ reason: item.reason,
1728
+ input: item.input,
1729
+ result: includeResults ? compactToolResult(item.result, 180) : undefined,
1811
1730
  }));
1812
1731
  const compactEntityTimelineForAgent = (timeline = [], maxItems = 8) => pruneByLimit(timeline || [], maxItems).map((item) => cleanContextValue({
1813
1732
  entity: item.entity || item.source || item.name,
@@ -2025,8 +1944,8 @@ const buildContextMessages = ({ payloadFormat, query, userId, profileFacts, work
2025
1944
  section: 'context_header',
2026
1945
  title: 'Tembory context',
2027
1946
  value: compactForAgent || compactStateSections
2028
- ? '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.'
2029
- : '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.',
2030
1949
  });
2031
1950
  }
2032
1951
  sections.push({
@@ -2162,9 +2081,9 @@ const buildContextMessages = ({ payloadFormat, query, userId, profileFacts, work
2162
2081
  value: {
2163
2082
  blocked_tools: guard.blockedTools,
2164
2083
  reasons: guard.reasons,
2165
- instruction: guard.blockedTools.length
2084
+ instruction: guard.instruction || (guard.blockedTools.length
2166
2085
  ? `Do not call these tools now: ${guard.blockedTools.join(', ')}. Ask for the missing context or execute the prerequisite step first.`
2167
- : 'No downstream tool is blocked by missing memory prerequisites.',
2086
+ : 'No downstream tool is blocked by missing memory prerequisites.'),
2168
2087
  },
2169
2088
  });
2170
2089
  sections.push({
@@ -3690,7 +3609,6 @@ exports.__private = {
3690
3609
  explicitToolHistoryItemsFromMemory,
3691
3610
  toolHistoryFromMemory,
3692
3611
  cleanAssistantTranscriptText,
3693
- availabilityFromRecentMessages,
3694
3612
  buildActionDirective,
3695
3613
  recentMessageFromMemory,
3696
3614
  previousUserFallbackFromWorkingMemory,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "n8n-nodes-tembory",
3
- "version": "1.0.30",
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));