n8n-nodes-tembory 1.1.42 → 1.1.44

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,20 @@
2
2
 
3
3
  Node de memoria operacional da Tembory para agentes de IA no n8n.
4
4
 
5
- Versao atual: `1.1.42`.
5
+ Versao atual: `1.1.44`.
6
+
7
+ ## 1.1.44
8
+
9
+ - Adiciona `toolReuseGuard` no contexto compacto do agente para reduzir repeticao indevida de tools entre turnos.
10
+ - Registra chamadas bem sucedidas recentes com `input_hash` e classifica tools de leitura versus side effect de forma generica.
11
+ - Instrui o agente a consultar `tool_ledger` antes de nova tool e a nao repetir side effects sem pedido explicito do usuario.
12
+ - Remove duplicacoes do payload compacto para manter o contexto balanceado abaixo do limite de regressao.
13
+
14
+ ## 1.1.43
15
+
16
+ - Adiciona `toolCapture` no payload visual de `saveContext`, deixando explicito quando nenhuma execucao de tool chegou para a memoria.
17
+ - Suporta tambem `intermediate_steps` em snake case, alem de `intermediateSteps`.
18
+ - Quando `toolCallsCaptured` for `0`, o payload informa que e necessario o AI Agent entregar `intermediateSteps`, `__temboryToolCalls` ou tool messages para a memoria conseguir salvar a tool.
6
19
 
7
20
  ## 1.1.42
8
21
 
@@ -1210,9 +1210,14 @@ const extractToolCalls = (outputValues = {}) => {
1210
1210
  source: 'tool_object',
1211
1211
  });
1212
1212
  }
1213
- if (Array.isArray(obj.intermediateSteps)) {
1214
- for (let index = 0; index < obj.intermediateSteps.length; index++) {
1215
- const step = obj.intermediateSteps[index];
1213
+ const intermediateSteps = Array.isArray(obj.intermediateSteps)
1214
+ ? obj.intermediateSteps
1215
+ : Array.isArray(obj.intermediate_steps)
1216
+ ? obj.intermediate_steps
1217
+ : undefined;
1218
+ if (Array.isArray(intermediateSteps)) {
1219
+ for (let index = 0; index < intermediateSteps.length; index++) {
1220
+ const step = intermediateSteps[index];
1216
1221
  if (Array.isArray(step) && step.length >= 1) {
1217
1222
  const action = step[0] || {};
1218
1223
  push(action.tool || action.name, action.toolInput || action.input || action.args, step[1], true, {
@@ -2167,17 +2172,56 @@ const shouldCarryPreviousGoal = (intent = '', previousGoal = '') => {
2167
2172
  return false;
2168
2173
  return ['general_message', 'profile_update', 'unknown'].includes(intent);
2169
2174
  };
2170
- const toolNameSuggestsSideEffect = (name = '') => /(?:^|[_\-. ])(book|booking|reserv\w*|agend\w*|confirm\w*|cancel\w*|cri\w*|create|update|upsert|set|send|enviar|registr\w*|cadastr\w*|charge|cobran\w*|pagamento|ticket)(?:$|[_\-. ])/i.test(String(name || ''));
2171
- const deriveToolRepeatPolicy = ({ intent = '', query = '', executedTools = [], toolState = {} }) => {
2175
+ const toolNameSuggestsSideEffect = (name = '') => {
2176
+ const text = String(name || '');
2177
+ if (/(?:^|[_\-. ])(check|consult\w*|search|lookup|find|list|get|read|info|inform\w*|disponibilidade|availability)(?:$|[_\-. ])/i.test(text))
2178
+ return false;
2179
+ return /(?:^|[_\-. ])(book|booking|reserv\w*|agend\w*|confirm\w*|cancel\w*|cri\w*|create|update|upsert|set|send|enviar|registr\w*|cadastr\w*|charge|cobran\w*|pagamento|ticket)(?:$|[_\-. ])/i.test(text);
2180
+ };
2181
+ const compactToolPolicyPayload = (value, max = 420) => {
2182
+ const parsed = safeParseToolPayload(value);
2183
+ const cleaned = stripNoisyToolFields(parsed);
2184
+ return truncate(typeof cleaned === 'string' ? cleaned : safeStringify(cleaned), max) || undefined;
2185
+ };
2186
+ const compactSuccessfulToolCallsForPolicy = (toolHistory = [], toolState = {}, maxItems = 10) => {
2187
+ const fromHistory = (Array.isArray(toolHistory) ? toolHistory : [])
2188
+ .filter((tool) => tool && tool.ok !== false && (tool.name || tool.tool_name || tool.tool))
2189
+ .map((tool) => {
2190
+ const name = String(tool.name || tool.tool_name || tool.tool || 'unknown_tool');
2191
+ return cleanContextValue({
2192
+ name,
2193
+ at: tool.at || tool.timestamp,
2194
+ input_hash: stableHash({ name, input: safeParseToolPayload(tool.input) }),
2195
+ side_effect: toolNameSuggestsSideEffect(name),
2196
+ });
2197
+ });
2198
+ const fromToolState = Object.values((toolState || {}).latest_by_name || {}).map((tool) => {
2199
+ const name = String(tool.name || tool.tool || 'unknown_tool');
2200
+ const input = compactToolPolicyPayload(tool.input, 220);
2201
+ return cleanContextValue({
2202
+ name,
2203
+ at: tool.at || tool.timestamp,
2204
+ input_hash: stableHash({ name, input }),
2205
+ side_effect: toolNameSuggestsSideEffect(name),
2206
+ });
2207
+ });
2208
+ return pruneByLimit(fromHistory.length ? fromHistory : fromToolState, maxItems);
2209
+ };
2210
+ const deriveToolRepeatPolicy = ({ intent = '', query = '', executedTools = [], toolState = {}, toolHistory = [] }) => {
2172
2211
  const tools = Array.from(new Set(executedTools.filter(Boolean)));
2173
2212
  const strictMemoryOnlyTurn = intent === 'conversation_recall' || intent === 'operational_status_question' || hasNoToolRequested(query);
2174
2213
  const sideEffectToolCandidates = tools.filter(toolNameSuggestsSideEffect);
2214
+ const successfulToolCalls = compactSuccessfulToolCallsForPolicy(toolHistory, toolState, 10);
2215
+ const referenceToolCandidates = tools.filter((tool) => !toolNameSuggestsSideEffect(tool));
2175
2216
  return cleanContextValue({
2176
2217
  mode: strictMemoryOnlyTurn ? 'answer_from_memory_when_possible' : 'conditional_reuse',
2177
2218
  avoid_repeating_tools_unless_needed: tools,
2178
2219
  legacy_do_not_repeat_applies: strictMemoryOnlyTurn,
2179
2220
  side_effect_tool_candidates: sideEffectToolCandidates,
2180
- instruction: 'Prior tools are evidence, not forbidden tools. Reuse outputs when sufficient; call tools for fresh data, new scope, or current side effects.',
2221
+ do_not_repeat_side_effect_tools_without_explicit_new_request: sideEffectToolCandidates,
2222
+ answer_from_memory_candidates: referenceToolCandidates,
2223
+ successful_tool_calls: successfulToolCalls,
2224
+ instruction: 'Check tool_ledger before any tool. If same tool/input already succeeded and user did not request new or fresh data, answer from prior result. Never repeat side-effect tools without an explicit new user request.',
2181
2225
  });
2182
2226
  };
2183
2227
  const deriveWorkingMemory = ({ query = '', profileFacts = {}, recentMessages = [], toolHistory = [], operationalState = {}, previous = {} }) => {
@@ -2273,7 +2317,7 @@ const deriveDecisionState = ({ query = '', toolHistory = [], operationalState =
2273
2317
  });
2274
2318
  };
2275
2319
  const executedTools = Array.from(new Set(Object.keys(toolState.counts_by_name || {})));
2276
- const toolRepeatPolicy = deriveToolRepeatPolicy({ intent, query, executedTools, toolState });
2320
+ const toolRepeatPolicy = deriveToolRepeatPolicy({ intent, query, executedTools, toolState, toolHistory });
2277
2321
  const doNotRepeatTools = toolRepeatPolicy.legacy_do_not_repeat_applies ? executedTools : [];
2278
2322
  if (toolState.last_successful_tool)
2279
2323
  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 });
@@ -2809,8 +2853,9 @@ const compactToolDecisionStateForAgent = (toolDecisionState = {}) => {
2809
2853
  };
2810
2854
  const compactRepeatToolPolicyForAgent = (policy = {}) => cleanContextValue({
2811
2855
  mode: policy.mode,
2812
- legacy_do_not_repeat_applies: policy.legacy_do_not_repeat_applies,
2813
- side_effect_tool_candidates: (policy.side_effect_tool_candidates || []).slice(0, 12),
2856
+ do_not_repeat_side_effect_tools: (policy.do_not_repeat_side_effect_tools_without_explicit_new_request || []).slice(0, 12),
2857
+ answer_from_memory_candidates: (policy.answer_from_memory_candidates || []).slice(0, 12),
2858
+ successful_tool_calls: (policy.successful_tool_calls || []).slice(-8),
2814
2859
  instruction: policy.instruction,
2815
2860
  });
2816
2861
  const compactDecisionStateForAgent = (state = {}) => cleanContextValue({
@@ -2825,9 +2870,6 @@ const compactDecisionStateForAgent = (state = {}) => cleanContextValue({
2825
2870
  at: decision.at || decision.updated_at,
2826
2871
  })),
2827
2872
  do_not_repeat_tools: state.do_not_repeat_tools,
2828
- repeat_tool_policy: state.repeat_tool_policy ? compactRepeatToolPolicyForAgent(state.repeat_tool_policy) : undefined,
2829
- avoid_repeating_tools_unless_needed: state.avoid_repeating_tools_unless_needed,
2830
- tool_decision_state: state.tool_decision_state ? compactToolDecisionStateForAgent(state.tool_decision_state) : undefined,
2831
2873
  latest_tool: state.latest_tool ? cleanContextValue({
2832
2874
  name: state.latest_tool.name,
2833
2875
  status: state.latest_tool.status || (state.latest_tool.ok === false ? 'failed' : 'ok'),
@@ -3187,6 +3229,36 @@ const conversationTimelineFromMessagesForSideChannel = (messages = [], maxItems
3187
3229
  preview: truncate(message.content, 180),
3188
3230
  }));
3189
3231
  };
3232
+ const hasToolCaptureSignal = (output = {}) => {
3233
+ let hasIntermediateSteps = false;
3234
+ let hasExplicitToolCalls = false;
3235
+ let hasToolObject = false;
3236
+ readDeep(output || {}, (obj) => {
3237
+ if (Array.isArray(obj.intermediateSteps) || Array.isArray(obj.intermediate_steps))
3238
+ hasIntermediateSteps = true;
3239
+ if (Array.isArray(obj.__temboryToolCalls) || Array.isArray(obj.toolCalls) || Array.isArray(obj.tool_calls))
3240
+ hasExplicitToolCalls = true;
3241
+ const name = obj.tool || obj.toolName || obj.name;
3242
+ const hasInput = obj.toolInput !== undefined || obj.input !== undefined || obj.args !== undefined;
3243
+ const hasResult = obj.observation !== undefined || obj.result !== undefined || obj.output !== undefined || obj.error !== undefined;
3244
+ if (name && hasInput && hasResult)
3245
+ hasToolObject = true;
3246
+ });
3247
+ return { hasIntermediateSteps, hasExplicitToolCalls, hasToolObject };
3248
+ };
3249
+ const summarizeToolCaptureForSideChannel = (output = {}, toolCalls = []) => {
3250
+ const signals = hasToolCaptureSignal(output);
3251
+ const sources = Array.from(new Set((toolCalls || []).map((tool) => tool.source).filter(Boolean)));
3252
+ return cleanContextValue({
3253
+ status: toolCalls.length ? 'captured' : 'no_tool_payload_received',
3254
+ captured: toolCalls.length,
3255
+ sources,
3256
+ signals,
3257
+ note: toolCalls.length
3258
+ ? undefined
3259
+ : 'No tool execution data was received by memory in this saveContext call. If a tool ran, enable AI Agent Return Intermediate Steps or pass tool messages to memory.',
3260
+ });
3261
+ };
3190
3262
  const summarizeSaveContextForSideChannel = (input = {}, output = {}, chatHistory = []) => {
3191
3263
  const inputMessage = input?.input || input?.chatInput || input?.query || input?.question || input?.text || input?.message || '';
3192
3264
  const outputMessage = output?.output || output?.response || output?.text || output?.message || output?.answer || '';
@@ -3204,6 +3276,7 @@ const summarizeSaveContextForSideChannel = (input = {}, output = {}, chatHistory
3204
3276
  assistantOutput: outputMessage ? truncate(String(outputMessage), 700) : undefined,
3205
3277
  toolCallsCaptured: toolCalls.length,
3206
3278
  toolNames: toolEvents.map((tool) => tool.name).filter(Boolean),
3279
+ toolCapture: summarizeToolCaptureForSideChannel(output, toolCalls),
3207
3280
  toolLog: buildToolLogForSideChannel(toolEvents, toolCalls.length, SIDE_CHANNEL_SAVE_TOOL_EVENT_MAX),
3208
3281
  toolEvents,
3209
3282
  }));
@@ -3664,7 +3737,6 @@ const buildContextMessages = ({ payloadFormat, query, userId, profileFacts, work
3664
3737
  value: cleanContextValue({
3665
3738
  current_intent: (decisionState || {}).current_intent || workingMemory.last_user_intent,
3666
3739
  latest_tool: ((decisionState || {}).latest_tool || (operationalState || {}).last_tool || undefined),
3667
- repeat_tool_policy: (decisionState || {}).repeat_tool_policy,
3668
3740
  avoid_repeating_tools_unless_needed: ((decisionState || {}).avoid_repeating_tools_unless_needed || []).slice(0, 12),
3669
3741
  do_not_repeat_tools_legacy: ((decisionState || {}).do_not_repeat_tools || []).slice(0, 12),
3670
3742
  instruction: actionDirective || workingMemory.next_expected_action || 'Continue according to the agent prompt.',
@@ -3721,6 +3793,7 @@ const buildContextMessages = ({ payloadFormat, query, userId, profileFacts, work
3721
3793
  state: minimalState,
3722
3794
  workingMemory: sectionValue('working_memory'),
3723
3795
  decisionState: sectionValue('decision_state'),
3796
+ toolReuseGuard: compactRepeatToolPolicyForAgent((decisionState || {}).repeat_tool_policy || {}),
3724
3797
  operationalState: sectionValue('operational_state'),
3725
3798
  actionLedger: sectionValue('action_ledger'),
3726
3799
  memoryCompression: sectionValue('memory_compression'),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "n8n-nodes-tembory",
3
- "version": "1.1.42",
3
+ "version": "1.1.44",
4
4
  "description": "Tembory node for n8n AI Agents with operational memory, tool history and decision state",
5
5
  "license": "MIT",
6
6
  "homepage": "https://tembory.com",