n8n-nodes-tembory 1.1.43 → 1.2.0

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,22 @@
2
2
 
3
3
  Node de memoria operacional da Tembory para agentes de IA no n8n.
4
4
 
5
- Versao atual: `1.1.43`.
5
+ Versao atual: `1.2.0`.
6
+
7
+ ## 1.2.0
8
+
9
+ - Remodela o contexto compacto do Agent para `tembory.agent_context.v2`, com contrato unico para conversa, fatos canonicos, evidencias de tools, politica de reuso, frescor, captura e budget.
10
+ - Remove duplicacao estrutural de mensagens no prompt do Agent: a conversa passa por `conversationFrame` e nao e repetida em blocos equivalentes.
11
+ - Preserva inputs, outputs, timestamps, fatos extraidos e resultados parseados de tools em `toolEvidence`, incluindo tools de busca vetorial quando o n8n entrega o payload.
12
+ - Mantem `toolReuseGuard` generico para evitar repeticao indevida de side effects e reutilizar outputs anteriores como evidencia sem transformar memoria em ToolMessage.
13
+ - Enriquece o resumo visual do n8n lendo o novo schema v2 sem vazar o `chatHistory` bruto no output do Agent.
14
+
15
+ ## 1.1.44
16
+
17
+ - Adiciona `toolReuseGuard` no contexto compacto do agente para reduzir repeticao indevida de tools entre turnos.
18
+ - Registra chamadas bem sucedidas recentes com `input_hash` e classifica tools de leitura versus side effect de forma generica.
19
+ - Instrui o agente a consultar `tool_ledger` antes de nova tool e a nao repetir side effects sem pedido explicito do usuario.
20
+ - Remove duplicacoes do payload compacto para manter o contexto balanceado abaixo do limite de regressao.
6
21
 
7
22
  ## 1.1.43
8
23
 
@@ -2172,17 +2172,56 @@ const shouldCarryPreviousGoal = (intent = '', previousGoal = '') => {
2172
2172
  return false;
2173
2173
  return ['general_message', 'profile_update', 'unknown'].includes(intent);
2174
2174
  };
2175
- 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 || ''));
2176
- 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 = [] }) => {
2177
2211
  const tools = Array.from(new Set(executedTools.filter(Boolean)));
2178
2212
  const strictMemoryOnlyTurn = intent === 'conversation_recall' || intent === 'operational_status_question' || hasNoToolRequested(query);
2179
2213
  const sideEffectToolCandidates = tools.filter(toolNameSuggestsSideEffect);
2214
+ const successfulToolCalls = compactSuccessfulToolCallsForPolicy(toolHistory, toolState, 10);
2215
+ const referenceToolCandidates = tools.filter((tool) => !toolNameSuggestsSideEffect(tool));
2180
2216
  return cleanContextValue({
2181
2217
  mode: strictMemoryOnlyTurn ? 'answer_from_memory_when_possible' : 'conditional_reuse',
2182
2218
  avoid_repeating_tools_unless_needed: tools,
2183
2219
  legacy_do_not_repeat_applies: strictMemoryOnlyTurn,
2184
2220
  side_effect_tool_candidates: sideEffectToolCandidates,
2185
- 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.',
2186
2225
  });
2187
2226
  };
2188
2227
  const deriveWorkingMemory = ({ query = '', profileFacts = {}, recentMessages = [], toolHistory = [], operationalState = {}, previous = {} }) => {
@@ -2278,7 +2317,7 @@ const deriveDecisionState = ({ query = '', toolHistory = [], operationalState =
2278
2317
  });
2279
2318
  };
2280
2319
  const executedTools = Array.from(new Set(Object.keys(toolState.counts_by_name || {})));
2281
- const toolRepeatPolicy = deriveToolRepeatPolicy({ intent, query, executedTools, toolState });
2320
+ const toolRepeatPolicy = deriveToolRepeatPolicy({ intent, query, executedTools, toolState, toolHistory });
2282
2321
  const doNotRepeatTools = toolRepeatPolicy.legacy_do_not_repeat_applies ? executedTools : [];
2283
2322
  if (toolState.last_successful_tool)
2284
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 });
@@ -2814,10 +2853,32 @@ const compactToolDecisionStateForAgent = (toolDecisionState = {}) => {
2814
2853
  };
2815
2854
  const compactRepeatToolPolicyForAgent = (policy = {}) => cleanContextValue({
2816
2855
  mode: policy.mode,
2817
- legacy_do_not_repeat_applies: policy.legacy_do_not_repeat_applies,
2818
- 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),
2819
2859
  instruction: policy.instruction,
2820
2860
  });
2861
+ const repeatToolPolicyForAgentContext = (decisionState = {}, toolHistory = []) => {
2862
+ if (decisionState.repeat_tool_policy)
2863
+ return decisionState.repeat_tool_policy;
2864
+ const legacy = Array.isArray(decisionState.do_not_repeat_tools) ? decisionState.do_not_repeat_tools : [];
2865
+ if (!legacy.length)
2866
+ return {};
2867
+ return cleanContextValue({
2868
+ mode: 'conditional_reuse',
2869
+ do_not_repeat_side_effect_tools_without_explicit_new_request: legacy.filter((name) => toolNameSuggestsSideEffect(name)),
2870
+ answer_from_memory_candidates: legacy,
2871
+ successful_tool_calls: (toolHistory || [])
2872
+ .filter((tool) => legacy.includes(tool.name || tool.tool_name || tool.tool) && tool.ok !== false)
2873
+ .map((tool) => ({
2874
+ name: tool.name || tool.tool_name || tool.tool,
2875
+ at: tool.at || tool.timestamp,
2876
+ input_hash: stableHash({ name: tool.name || tool.tool_name || tool.tool, input: safeParseToolPayload(tool.input) }),
2877
+ side_effect: toolNameSuggestsSideEffect(tool.name || tool.tool_name || tool.tool),
2878
+ })),
2879
+ 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.',
2880
+ });
2881
+ };
2821
2882
  const compactDecisionStateForAgent = (state = {}) => cleanContextValue({
2822
2883
  current_intent: state.current_intent,
2823
2884
  active_decisions: (state.active_decisions || []).slice(-4).map((decision) => cleanContextValue({
@@ -2830,9 +2891,6 @@ const compactDecisionStateForAgent = (state = {}) => cleanContextValue({
2830
2891
  at: decision.at || decision.updated_at,
2831
2892
  })),
2832
2893
  do_not_repeat_tools: state.do_not_repeat_tools,
2833
- repeat_tool_policy: state.repeat_tool_policy ? compactRepeatToolPolicyForAgent(state.repeat_tool_policy) : undefined,
2834
- avoid_repeating_tools_unless_needed: state.avoid_repeating_tools_unless_needed,
2835
- tool_decision_state: state.tool_decision_state ? compactToolDecisionStateForAgent(state.tool_decision_state) : undefined,
2836
2894
  latest_tool: state.latest_tool ? cleanContextValue({
2837
2895
  name: state.latest_tool.name,
2838
2896
  status: state.latest_tool.status || (state.latest_tool.ok === false ? 'failed' : 'ok'),
@@ -2937,6 +2995,234 @@ const compactVectorMemoriesForAgent = (vectorMemories = [], toolHistory = [], ma
2937
2995
  .filter((text) => !hasStructuredTools || !/^\[Used tools:/i.test(text))
2938
2996
  .slice(0, maxItems);
2939
2997
  };
2998
+ const AGENT_CONTEXT_KIND_V2 = 'tembory.agent_context.v2';
2999
+ const compactStructuredPayloadForAgentV2 = (value, max = 900) => {
3000
+ const parsed = safeParseToolPayload(value);
3001
+ const cleaned = parsed && typeof parsed === 'object' ? stripNoisyToolFields(parsed) : parsed;
3002
+ const text = typeof cleaned === 'string' ? cleaned : safeStringify(cleaned);
3003
+ if (!text || text === 'undefined')
3004
+ return undefined;
3005
+ if (text.length <= max)
3006
+ return cleaned;
3007
+ return truncate(text, max);
3008
+ };
3009
+ const compactToolOutputForAgentV2 = (tool = {}, includeResults = true) => {
3010
+ if (includeResults === false)
3011
+ return undefined;
3012
+ const raw = tool.result !== undefined ? tool.result : tool.output !== undefined ? tool.output : tool.observation;
3013
+ const parsed = compactParsedToolOutputForSideChannel(safeParseToolPayload(raw));
3014
+ if (parsed !== undefined)
3015
+ return parsed;
3016
+ return compactToolResult(raw, 1000);
3017
+ };
3018
+ const compactToolEvidenceForAgentV2 = (toolHistory = [], maxItems = 8, includeResults = true) => {
3019
+ const tools = pruneByLimit(toolHistory || [], maxItems);
3020
+ const events = tools.map((tool, index) => {
3021
+ const name = String(tool.name || tool.tool_name || tool.tool || 'unknown_tool');
3022
+ return cleanContextValue({
3023
+ index,
3024
+ name,
3025
+ status: tool.ok === false ? 'failed' : 'ok',
3026
+ at: tool.at || tool.timestamp,
3027
+ input: compactStructuredPayloadForAgentV2(tool.input, 700),
3028
+ inputHash: stableHash({ name, input: safeParseToolPayload(tool.input) }),
3029
+ facts: extractToolOperationalFacts(tool),
3030
+ output: compactToolOutputForAgentV2(tool, includeResults),
3031
+ sideEffect: toolNameSuggestsSideEffect(name),
3032
+ source: tool.source,
3033
+ });
3034
+ });
3035
+ const names = Array.from(new Set((toolHistory || []).map((tool) => tool.name || tool.tool_name || tool.tool).filter(Boolean)));
3036
+ return cleanContextValue({
3037
+ count: Array.isArray(toolHistory) ? toolHistory.length : 0,
3038
+ shown: events.length,
3039
+ names: names.slice(0, 16),
3040
+ latest: events.length ? {
3041
+ name: events[events.length - 1].name,
3042
+ status: events[events.length - 1].status,
3043
+ at: events[events.length - 1].at,
3044
+ } : undefined,
3045
+ events,
3046
+ });
3047
+ };
3048
+ const compactConversationFrameForAgentV2 = ({ query = '', recentMessages = [], workingMemory = {}, maxItems = 10 }) => {
3049
+ const chronological = sortConversationChronological(recentMessages || []).map((message) => ({
3050
+ role: normalizeConversationRoleForSideChannel(message.role || message.type),
3051
+ at: message.at || message.timestamp || message.created_at || message.createdAt,
3052
+ content: truncate(message.content || message.text || message.message || '', 500),
3053
+ }));
3054
+ const currentNormalized = normalizeIntentText(query).trim();
3055
+ const users = chronological.filter((message) => message.role === 'user');
3056
+ const assistants = chronological.filter((message) => message.role === 'agent' || message.role === 'assistant' || message.role === 'ai');
3057
+ const currentUser = [...users].reverse().find((message) => normalizeIntentText(message.content).trim() === currentNormalized) || (query ? { role: 'user', content: truncate(query, 500), at: nowIso() } : users[users.length - 1]);
3058
+ const previousUser = [...users].reverse().find((message) => normalizeIntentText(message.content).trim() !== currentNormalized);
3059
+ const lastAssistant = assistants[assistants.length - 1] || null;
3060
+ return cleanContextValue({
3061
+ currentUserMessage: currentUser ? currentUser.content : truncate(query, 500),
3062
+ previousUserMessage: previousUser ? { content: previousUser.content, at: previousUser.at } : undefined,
3063
+ lastAssistantMessage: lastAssistant ? { content: lastAssistant.content, at: lastAssistant.at } : undefined,
3064
+ timeline: pruneByLimit(chronological, maxItems),
3065
+ counts: {
3066
+ messages: chronological.length,
3067
+ userMessages: users.length,
3068
+ assistantMessages: assistants.length,
3069
+ },
3070
+ instruction: 'This is the authoritative user/assistant transcript. Use it for recall questions before summaries, facts, or tool evidence.',
3071
+ digest: (workingMemory || {}).conversation_digest,
3072
+ });
3073
+ };
3074
+ const mergeLatestOperationalFacts = (toolHistory = []) => {
3075
+ let merged = {};
3076
+ for (const tool of toolHistory || []) {
3077
+ if (tool && tool.ok === false)
3078
+ continue;
3079
+ const facts = extractToolOperationalFacts(tool);
3080
+ if (facts)
3081
+ merged = mergeOperationalFactObjects(facts, merged);
3082
+ }
3083
+ return cleanContextValue(merged);
3084
+ };
3085
+ const buildCanonicalFactsForAgentV2 = ({ profileFacts = {}, toolHistory = [], operationalState = {} }) => {
3086
+ const toolState = (operationalState || {}).tool_state || {};
3087
+ const latestByTool = {};
3088
+ for (const [name, item] of Object.entries(toolState.latest_by_name || {})) {
3089
+ latestByTool[name] = cleanContextValue({
3090
+ at: item.at,
3091
+ status: item.ok === false ? 'failed' : 'ok',
3092
+ facts: item.facts,
3093
+ });
3094
+ }
3095
+ const sideEffects = (toolHistory || []).filter((tool) => tool && tool.ok !== false && toolNameSuggestsSideEffect(tool.name || tool.tool_name || tool.tool)).map((tool) => cleanContextValue({
3096
+ tool: tool.name || tool.tool_name || tool.tool,
3097
+ at: tool.at || tool.timestamp,
3098
+ facts: extractToolOperationalFacts(tool),
3099
+ output: compactToolOutputForAgentV2(tool, true),
3100
+ }));
3101
+ return cleanContextValue({
3102
+ profile: renderProfileFacts(profileFacts),
3103
+ operational: mergeLatestOperationalFacts(toolHistory),
3104
+ latestByTool,
3105
+ completedSideEffects: sideEffects.slice(-8),
3106
+ sourcePolicy: 'Structured tool outputs and tool facts beat vector text and summaries when they conflict.',
3107
+ });
3108
+ };
3109
+ const classifyToolFreshnessPolicy = (tool = {}, ttlSeconds = 0) => {
3110
+ const name = String(tool.name || tool.tool_name || tool.tool || '');
3111
+ if (tool.ok === false)
3112
+ return { ttlPolicy: 'failed_tool_short_memory', canReuseAsEvidence: true, shouldRefreshForNewAction: true };
3113
+ if (toolNameSuggestsSideEffect(name))
3114
+ return { ttlPolicy: 'completed_side_effect_until_changed', canReuseAsEvidence: true, shouldRefreshForNewAction: false };
3115
+ if (/(availability|disponibilidade|slot|horario|horário|agenda)/i.test(name))
3116
+ return { ttlPolicy: 'volatile_lookup', canReuseAsEvidence: true, shouldRefreshForNewAction: true };
3117
+ return { ttlPolicy: ttlSeconds > 0 ? 'bounded_reference' : 'stable_reference', canReuseAsEvidence: true, shouldRefreshForNewAction: false };
3118
+ };
3119
+ const buildFreshnessForAgentV2 = (toolHistory = [], adv = {}) => {
3120
+ const ttlSeconds = Number(adv.toolHistoryTTLSeconds || 0);
3121
+ const nowMs = Date.now();
3122
+ return cleanContextValue({
3123
+ generatedAt: nowIso(),
3124
+ toolHistoryTTLSeconds: ttlSeconds || undefined,
3125
+ tools: pruneByLimit(toolHistory || [], adv.toolHistoryLastN || 8).map((tool) => {
3126
+ const at = tool.at || tool.timestamp;
3127
+ const ageSeconds = at ? Math.max(0, Math.round((nowMs - new Date(at).getTime()) / 1000)) : undefined;
3128
+ return cleanContextValue({
3129
+ name: tool.name || tool.tool_name || tool.tool,
3130
+ at,
3131
+ ageSeconds,
3132
+ ...classifyToolFreshnessPolicy(tool, ttlSeconds),
3133
+ });
3134
+ }),
3135
+ });
3136
+ };
3137
+ const buildCaptureHealthForAgentV2 = ({ diagnostics = {}, toolHistory = [], recentMessages = [] }) => {
3138
+ const capture = (diagnostics || {}).captureState || {};
3139
+ const lastCaptured = Number(capture.last_save_tool_calls_captured || 0);
3140
+ const hasToolHistory = Array.isArray(toolHistory) && toolHistory.length > 0;
3141
+ return cleanContextValue({
3142
+ conversationCaptured: Array.isArray(recentMessages) && recentMessages.length > 0,
3143
+ toolPayloadReceived: lastCaptured > 0 || hasToolHistory,
3144
+ toolCallsCapturedLastSave: capture.last_save_tool_calls_captured,
3145
+ toolHistoryAvailable: hasToolHistory,
3146
+ lastSaveAt: capture.last_save_at,
3147
+ captureSources: capture.last_save_capture_sources,
3148
+ status: lastCaptured > 0 ? 'captured_in_last_save' : hasToolHistory ? 'historical_tool_context_available' : 'no_tool_payload_received',
3149
+ actionRequired: !hasToolHistory && lastCaptured === 0 ? 'Enable AI Agent Return Intermediate Steps or pass tool messages/__temboryToolCalls to memory so tools can be saved.' : undefined,
3150
+ });
3151
+ };
3152
+ const buildAgentContextV2 = ({ query = '', userId = '', payloadFormat = 'structured', profileFacts = {}, workingMemory = {}, decisionState = {}, memoryCompression = {}, operationalState = {}, actionLedger = [], vectorMemories = [], recentMessages = [], toolHistory = [], diagnostics = {}, activeSummary = '', connectedModelSummary = '', adv = {} }) => {
3153
+ const intent = (decisionState || {}).current_intent || (workingMemory || {}).last_user_intent || inferUserIntent(query, recentMessages);
3154
+ const toolEvidence = compactToolEvidenceForAgentV2(toolHistory, adv.toolHistoryLastN || 8, adv.includeToolResults !== false);
3155
+ const referenceFacts = compactVectorMemoriesForAgent(vectorMemories || [], toolHistory || [], Number(adv.summaryMaxFacts || 4));
3156
+ const captureHealth = buildCaptureHealthForAgentV2({ diagnostics, toolHistory, recentMessages });
3157
+ const statusQuestion = intent === 'operational_status_question';
3158
+ const recallOnly = intent === 'conversation_recall' || (statusQuestion && hasNoToolRequested(query));
3159
+ const actionDirective = buildActionDirective({ workingMemory, operationalState });
3160
+ return cleanContextValue({
3161
+ kind: AGENT_CONTEXT_KIND_V2,
3162
+ schemaVersion: '2.0',
3163
+ meta: {
3164
+ userId,
3165
+ payloadFormat,
3166
+ generatedAt: nowIso(),
3167
+ },
3168
+ instruction: {
3169
+ role: 'read_only_operational_memory',
3170
+ precedence: [
3171
+ 'The current user message and the agent prompt are authoritative for what to do now.',
3172
+ 'conversationFrame is authoritative for what the user and assistant said.',
3173
+ 'toolEvidence is authoritative for prior tool calls, inputs, outputs, status and timestamps.',
3174
+ 'canonicalFacts are derived from structured tool outputs and profile facts; use source tool facts over vector text when they conflict.',
3175
+ 'Do not represent a new side effect as completed from memory alone. New side effects require a current successful tool call when the agent prompt requires one.',
3176
+ ],
3177
+ langchainN8nContract: 'This is a SystemMessage context payload. Historical tools are evidence, not LangChain ToolMessages, and must not be replayed as tool results.',
3178
+ },
3179
+ currentTurn: {
3180
+ userMessage: truncate(query, 700),
3181
+ intentHint: intent,
3182
+ noToolRequested: hasNoToolRequested(query),
3183
+ recallOnly,
3184
+ statusQuestion,
3185
+ },
3186
+ conversationFrame: compactConversationFrameForAgentV2({
3187
+ query,
3188
+ recentMessages,
3189
+ workingMemory,
3190
+ maxItems: Number(adv.agentRecentMessagesLastN || adv.lastN || 10),
3191
+ }),
3192
+ canonicalFacts: buildCanonicalFactsForAgentV2({ profileFacts, toolHistory, operationalState }),
3193
+ toolEvidence,
3194
+ toolReuseGuard: compactRepeatToolPolicyForAgent(repeatToolPolicyForAgentContext(decisionState || {}, toolHistory || [])),
3195
+ actionDirective: actionDirective ? cleanContextValue({
3196
+ requiredTool: actionDirective.required_tool,
3197
+ evidenceFromRecentTools: actionDirective.evidence_from_recent_tools,
3198
+ instruction: actionDirective.instruction,
3199
+ }) : undefined,
3200
+ pendingState: {
3201
+ nextExpectedAction: (workingMemory || {}).next_expected_action,
3202
+ activeDecisions: compactDecisionStateForAgent(decisionState || {}).active_decisions,
3203
+ missingRequiredFields: [],
3204
+ },
3205
+ freshness: buildFreshnessForAgentV2(toolHistory, adv),
3206
+ memory: cleanContextValue({
3207
+ slmSummary: connectedModelSummary ? truncate(connectedModelSummary, Number(adv.connectedModelSummaryMaxChars || 900)) : undefined,
3208
+ activeSummary: !connectedModelSummary && activeSummary ? truncate(activeSummary, Number(adv.activeSummaryMaxChars || 900)) : undefined,
3209
+ referenceFacts,
3210
+ compression: adv.includeMemoryCompression === false ? undefined : compactMemoryCompressionForAgent(memoryCompression || {}),
3211
+ }),
3212
+ statusAnswerMaterial: statusQuestion ? buildStatusAnswerMaterialForAgent({ query, workingMemory, toolHistory, actionLedger }) : undefined,
3213
+ captureHealth,
3214
+ contextBudget: {
3215
+ approxTokens: approxTokenCount(safeStringify({
3216
+ query,
3217
+ recentMessages: pruneByLimit(recentMessages || [], Number(adv.agentRecentMessagesLastN || adv.lastN || 10)),
3218
+ toolEvidence,
3219
+ referenceFacts,
3220
+ })),
3221
+ conversationMessagesIncluded: Math.min((recentMessages || []).length, Number(adv.agentRecentMessagesLastN || adv.lastN || 10)),
3222
+ toolEventsIncluded: (toolEvidence.events || []).length,
3223
+ },
3224
+ });
3225
+ };
2940
3226
  const modelResponseText = (response) => {
2941
3227
  const content = response === null || response === void 0 ? void 0 : response.content;
2942
3228
  if (typeof content === 'string')
@@ -3270,15 +3556,15 @@ const compactMemoryEventPayload = (payload = {}) => {
3270
3556
  const compactLastSaveForSideChannel = (lastSave = {}) => cleanContextValue({
3271
3557
  saved: lastSave.saved,
3272
3558
  status: lastSave.status,
3273
- at: lastSave.at,
3274
- inputChars: lastSave.input_chars,
3275
- outputChars: lastSave.output_chars,
3276
- toolCallsCaptured: lastSave.tool_calls_captured,
3277
- toolNames: lastSave.tool_names,
3278
- conversationMessagesAfterSave: lastSave.conversation_messages_after_save,
3279
- toolHistoryAfterSave: lastSave.tool_history_after_save,
3280
- threadStateSaved: lastSave.thread_state_saved,
3281
- backendPersistence: lastSave.backend_memory_persistence,
3559
+ at: lastSave.at || lastSave.lastSaveAt,
3560
+ inputChars: lastSave.input_chars || lastSave.inputChars,
3561
+ outputChars: lastSave.output_chars || lastSave.outputChars,
3562
+ toolCallsCaptured: lastSave.tool_calls_captured || lastSave.toolCallsCaptured,
3563
+ toolNames: lastSave.tool_names || lastSave.toolNames,
3564
+ conversationMessagesAfterSave: lastSave.conversation_messages_after_save || lastSave.conversationMessagesAfterSave,
3565
+ toolHistoryAfterSave: lastSave.tool_history_after_save || lastSave.toolHistoryAfterSave,
3566
+ threadStateSaved: lastSave.thread_state_saved || lastSave.threadStateSaved,
3567
+ backendPersistence: lastSave.backend_memory_persistence || lastSave.backendPersistence,
3282
3568
  });
3283
3569
  const compactDedupeForSideChannel = (dedupe = {}) => {
3284
3570
  const load = dedupe.load || {};
@@ -3293,18 +3579,23 @@ const compactDedupeForSideChannel = (dedupe = {}) => {
3293
3579
  });
3294
3580
  };
3295
3581
  const loadedSectionsForSideChannel = (parsed = {}, memoryAudit = {}) => cleanContextValue({
3296
- conversation: Boolean(parsed.conversation),
3297
- summary: Boolean(parsed.summary),
3582
+ conversation: Boolean(parsed.conversation || parsed.conversationFrame),
3583
+ summary: Boolean(parsed.summary || parsed.memory),
3298
3584
  activeSummary: Boolean(parsed.activeSummary),
3299
3585
  connectedModelSummary: Boolean(parsed.connectedModelSummary),
3300
- workingMemory: Boolean(parsed.workingMemory),
3586
+ workingMemory: Boolean(parsed.workingMemory || parsed.pendingState),
3301
3587
  decisionState: Boolean(parsed.decisionState),
3302
- memoryCompression: Boolean(parsed.memoryCompression),
3588
+ memoryCompression: Boolean(parsed.memoryCompression || parsed.memory?.compression),
3303
3589
  operationalState: Boolean(parsed.operationalState),
3304
3590
  actionLedger: Array.isArray(parsed.actionLedger),
3305
3591
  statusAnswerMaterial: Boolean(parsed.statusAnswerMaterial || parsed.status_answer_material),
3306
3592
  turnBrief: Boolean(parsed.turnBrief || parsed.turn_brief),
3307
3593
  memoryAudit: Boolean(memoryAudit && Object.keys(memoryAudit).length),
3594
+ canonicalFacts: Boolean(parsed.canonicalFacts),
3595
+ toolEvidence: Boolean(parsed.toolEvidence),
3596
+ toolReuseGuard: Boolean(parsed.toolReuseGuard),
3597
+ freshness: Boolean(parsed.freshness),
3598
+ captureHealth: Boolean(parsed.captureHealth),
3308
3599
  entityTimeline: Array.isArray(parsed.entityTimeline),
3309
3600
  vectorMemories: Array.isArray(parsed.vectorMemories),
3310
3601
  graph: Array.isArray(parsed.graph),
@@ -3338,12 +3629,17 @@ const agentContextBudgetForSideChannel = (messages = [], parsed = {}) => {
3338
3629
  const totalChars = messageBudgets.reduce((sum, item) => sum + item.chars, 0);
3339
3630
  const sectionCandidates = [
3340
3631
  ['instruction', parsed.instruction],
3341
- ['conversation', parsed.conversation],
3632
+ ['conversation', parsed.conversation || parsed.conversationFrame],
3342
3633
  ['current_turn_focus', parsed.current_turn_focus || parsed.currentTurn || parsed.current_turn],
3634
+ ['canonical_facts', parsed.canonicalFacts],
3635
+ ['tool_evidence', parsed.toolEvidence],
3636
+ ['tool_reuse_guard', parsed.toolReuseGuard],
3637
+ ['freshness', parsed.freshness],
3638
+ ['capture_health', parsed.captureHealth],
3343
3639
  ['action_directive', parsed.action_directive],
3344
3640
  ['status_answer_material', parsed.statusAnswerMaterial || parsed.status_answer_material],
3345
3641
  ['turn_brief', parsed.turnBrief || parsed.turn_brief],
3346
- ['summary', parsed.summary],
3642
+ ['summary', parsed.summary || parsed.memory],
3347
3643
  ['working_memory', parsed.workingMemory || parsed.working_memory],
3348
3644
  ['decision_state', parsed.decisionState || parsed.decision_state],
3349
3645
  ['operational_state', parsed.operationalState || parsed.operational_state],
@@ -3382,7 +3678,7 @@ const summarizeMemoryMessagesForSideChannel = (messages = []) => {
3382
3678
  const type = messageTypeOf(message);
3383
3679
  return type === 'system' || /system/i.test(String(message?.role || ''));
3384
3680
  });
3385
- const contextMessage = firstSystem || list.find((message) => messageContentOf(message).includes('tembory.agent_context.v1')) || list[0];
3681
+ const contextMessage = firstSystem || list.find((message) => /tembory\.agent_context\.v[12]/.test(messageContentOf(message))) || list[0];
3386
3682
  if (!contextMessage)
3387
3683
  return summary;
3388
3684
  try {
@@ -3398,11 +3694,35 @@ const summarizeMemoryMessagesForSideChannel = (messages = []) => {
3398
3694
  throw new Error('Tembory visual summary could not find JSON context');
3399
3695
  parsed = JSON.parse(rawContent.slice(start, end + 1));
3400
3696
  }
3401
- const conversation = parsed.conversation || {};
3402
- const tools = parsed.tools || {};
3403
- const toolItems = Array.isArray(tools.items) ? tools.items : [];
3404
- const memoryAudit = parsed.memoryAudit || parsed.memory_audit || {};
3405
- const summaryText = parsed.summary?.slm || parsed.summary || parsed.connectedModelSummary || parsed.activeSummary || '';
3697
+ const isAgentContextV2 = parsed.kind === AGENT_CONTEXT_KIND_V2;
3698
+ const rawConversation = parsed.conversation || parsed.conversationFrame || {};
3699
+ const conversation = isAgentContextV2 ? cleanContextValue({
3700
+ current_user_message: rawConversation.currentUserMessage,
3701
+ conversation_history_chronological: (rawConversation.timeline || []).map((message) => ({
3702
+ role: message.role,
3703
+ content: message.content,
3704
+ at: message.at,
3705
+ })),
3706
+ all_user_messages_chronological: (rawConversation.timeline || []).filter((message) => normalizeConversationRoleForSideChannel(message.role) === 'user').map((message) => ({
3707
+ role: 'user',
3708
+ content: message.content,
3709
+ at: message.at,
3710
+ })),
3711
+ }) : rawConversation;
3712
+ const tools = parsed.tools || parsed.toolEvidence || {};
3713
+ const rawToolItems = Array.isArray(tools.items) ? tools.items : (Array.isArray(tools.events) ? tools.events : []);
3714
+ const toolItems = rawToolItems.map((tool) => cleanContextValue({
3715
+ name: tool.name || tool.tool_name || tool.tool,
3716
+ status: tool.status,
3717
+ ok: tool.ok !== false && tool.status !== 'failed',
3718
+ at: tool.at || tool.timestamp,
3719
+ input: tool.input,
3720
+ result: tool.result !== undefined ? tool.result : tool.output,
3721
+ facts: tool.facts,
3722
+ source: tool.source,
3723
+ }));
3724
+ const memoryAudit = parsed.memoryAudit || parsed.memory_audit || parsed.captureHealth || {};
3725
+ const summaryText = parsed.summary?.slm || parsed.summary || parsed.connectedModelSummary || parsed.activeSummary || parsed.memory?.slmSummary || parsed.memory?.activeSummary || '';
3406
3726
  const chronological = Array.isArray(conversation.conversation_history_chronological)
3407
3727
  ? conversation.conversation_history_chronological
3408
3728
  : [];
@@ -3412,7 +3732,11 @@ const summarizeMemoryMessagesForSideChannel = (messages = []) => {
3412
3732
  const lastUser = [...chronological].reverse().find((message) => normalizeConversationRoleForSideChannel(message.role || message.type) === 'user');
3413
3733
  const lastAgent = [...chronological].reverse().find((message) => normalizeConversationRoleForSideChannel(message.role || message.type) === 'agent');
3414
3734
  const includeDebug = Boolean(parsed.options?.includeDiagnostics || parsed.diagnostics?.includeDiagnostics || parsed.diagnostics?.include_diagnostics);
3415
- const lastSave = compactLastSaveForSideChannel(memoryAudit.last_save || parsed.state?.last_save || {});
3735
+ const lastSave = compactLastSaveForSideChannel(memoryAudit.last_save || parsed.state?.last_save || (isAgentContextV2 ? {
3736
+ at: parsed.captureHealth?.lastSaveAt,
3737
+ toolCallsCaptured: parsed.captureHealth?.toolCallsCapturedLastSave,
3738
+ captureSources: parsed.captureHealth?.captureSources,
3739
+ } : {}));
3416
3740
  const fullDedupeSummary = parsed.dedupeSummary || parsed.diagnostics?.dedupeSummary || undefined;
3417
3741
  const loadedSections = loadedSectionsForSideChannel(parsed, memoryAudit);
3418
3742
  const agentContextBudget = agentContextBudgetForSideChannel(list, parsed);
@@ -3421,7 +3745,7 @@ const summarizeMemoryMessagesForSideChannel = (messages = []) => {
3421
3745
  summary.project = parsed.project || undefined;
3422
3746
  summary.retrievalMode = parsed.retrievalMode;
3423
3747
  summary.payloadFormat = parsed.payloadFormat;
3424
- summary.intent = parsed.observations?.inferred_intent?.label || parsed.workingMemory?.last_user_intent || parsed.decisionState?.current_intent || undefined;
3748
+ summary.intent = parsed.currentTurn?.intentHint || parsed.observations?.inferred_intent?.label || parsed.workingMemory?.last_user_intent || parsed.decisionState?.current_intent || undefined;
3425
3749
  const parsedTurnBrief = parsed.turnBrief || parsed.turn_brief || undefined;
3426
3750
  summary.currentTurn = parsed.currentTurn || parsed.current_turn || parsed.diagnostics?.currentTurn || undefined;
3427
3751
  summary.currentUserMessage = conversation.current_user_message ? truncate(conversation.current_user_message, 180) : undefined;
@@ -3476,29 +3800,37 @@ const summarizeMemoryMessagesForSideChannel = (messages = []) => {
3476
3800
  summary.lastSave = Object.keys(lastSave).length ? lastSave : undefined;
3477
3801
  summary.dedupe = fullDedupeSummary ? compactDedupeForSideChannel(fullDedupeSummary) : undefined;
3478
3802
  summary.workingMemory = cleanContextValue({
3479
- currentGoal: parsed.workingMemory?.current_goal,
3803
+ currentGoal: parsed.workingMemory?.current_goal || parsed.pendingState?.nextExpectedAction,
3480
3804
  currentGoalReason: parsed.workingMemory?.current_goal_reason,
3481
- currentTask: parsed.workingMemory?.current_task,
3482
- nextExpectedAction: parsed.workingMemory?.next_expected_action,
3483
- lastUserIntent: parsed.workingMemory?.last_user_intent,
3484
- lastUserMessage: parsed.workingMemory?.last_user_message ? truncate(parsed.workingMemory.last_user_message, 220) : undefined,
3485
- conversation: parsed.workingMemory?.conversation_digest,
3486
- tools: parsed.workingMemory?.tool_digest,
3487
- turnFlags: parsed.workingMemory?.turn_flags,
3805
+ currentTask: parsed.workingMemory?.current_task || parsed.pendingState?.nextExpectedAction,
3806
+ nextExpectedAction: parsed.workingMemory?.next_expected_action || parsed.pendingState?.nextExpectedAction,
3807
+ lastUserIntent: parsed.workingMemory?.last_user_intent || parsed.currentTurn?.intentHint,
3808
+ lastUserMessage: parsed.workingMemory?.last_user_message ? truncate(parsed.workingMemory.last_user_message, 220) : parsed.currentTurn?.userMessage,
3809
+ conversation: parsed.workingMemory?.conversation_digest || parsed.conversationFrame?.counts,
3810
+ tools: parsed.workingMemory?.tool_digest || { total: parsed.toolEvidence?.count, names: parsed.toolEvidence?.names },
3811
+ turnFlags: parsed.workingMemory?.turn_flags || cleanContextValue({
3812
+ recall_only: parsed.currentTurn?.recallOnly,
3813
+ status_question: parsed.currentTurn?.statusQuestion,
3814
+ no_tool_requested: parsed.currentTurn?.noToolRequested,
3815
+ has_prior_tools: parsed.toolEvidence?.count ? true : undefined,
3816
+ }),
3488
3817
  contextSources: parsed.workingMemory?.context_sources,
3489
3818
  agentGuidance: parsed.workingMemory?.agent_guidance,
3490
3819
  });
3491
3820
  summary.decisionState = cleanContextValue({
3492
- currentIntent: parsed.decisionState?.current_intent,
3821
+ currentIntent: parsed.decisionState?.current_intent || parsed.currentTurn?.intentHint,
3493
3822
  decisionCount: parsed.decisionState?.decision_count,
3494
- latestTool: parsed.decisionState?.latest_tool,
3495
- doNotRepeatTools: parsed.decisionState?.do_not_repeat_tools,
3823
+ latestTool: parsed.decisionState?.latest_tool || parsed.toolEvidence?.latest,
3824
+ doNotRepeatTools: parsed.decisionState?.do_not_repeat_tools || parsed.toolReuseGuard?.do_not_repeat_side_effect_tools,
3496
3825
  avoidRepeatingToolsUnlessNeeded: parsed.decisionState?.avoid_repeating_tools_unless_needed,
3497
3826
  repeatToolPolicy: parsed.decisionState?.repeat_tool_policy ? {
3498
3827
  mode: parsed.decisionState.repeat_tool_policy.mode,
3499
3828
  legacyDoNotRepeatApplies: parsed.decisionState.repeat_tool_policy.legacy_do_not_repeat_applies,
3500
3829
  sideEffectToolCandidates: parsed.decisionState.repeat_tool_policy.side_effect_tool_candidates,
3501
- } : undefined,
3830
+ } : (parsed.toolReuseGuard ? {
3831
+ mode: parsed.toolReuseGuard.mode,
3832
+ sideEffectToolCandidates: parsed.toolReuseGuard.do_not_repeat_side_effect_tools,
3833
+ } : undefined),
3502
3834
  });
3503
3835
  summary.quality = parsed.contextHealth?.quality_score || parsed.contextQualityScore || undefined;
3504
3836
  summary.debug = includeDebug ? cleanContextValue({
@@ -3700,7 +4032,6 @@ const buildContextMessages = ({ payloadFormat, query, userId, profileFacts, work
3700
4032
  value: cleanContextValue({
3701
4033
  current_intent: (decisionState || {}).current_intent || workingMemory.last_user_intent,
3702
4034
  latest_tool: ((decisionState || {}).latest_tool || (operationalState || {}).last_tool || undefined),
3703
- repeat_tool_policy: (decisionState || {}).repeat_tool_policy,
3704
4035
  avoid_repeating_tools_unless_needed: ((decisionState || {}).avoid_repeating_tools_unless_needed || []).slice(0, 12),
3705
4036
  do_not_repeat_tools_legacy: ((decisionState || {}).do_not_repeat_tools || []).slice(0, 12),
3706
4037
  instruction: actionDirective || workingMemory.next_expected_action || 'Continue according to the agent prompt.',
@@ -3734,6 +4065,24 @@ const buildContextMessages = ({ payloadFormat, query, userId, profileFacts, work
3734
4065
  context_quality_score: diagnostics?.contextHealth?.quality_score || diagnostics?.quality_score,
3735
4066
  last_save: memoryAudit?.last_save,
3736
4067
  });
4068
+ const agentContextV2 = buildAgentContextV2({
4069
+ query,
4070
+ userId,
4071
+ payloadFormat,
4072
+ profileFacts,
4073
+ workingMemory,
4074
+ decisionState,
4075
+ memoryCompression,
4076
+ operationalState,
4077
+ actionLedger,
4078
+ vectorMemories,
4079
+ recentMessages,
4080
+ toolHistory,
4081
+ diagnostics,
4082
+ activeSummary,
4083
+ connectedModelSummary,
4084
+ adv,
4085
+ });
3737
4086
  const compactJson = cleanContextValue({
3738
4087
  kind: 'tembory.agent_context.v1',
3739
4088
  instruction: includeHeader ? `${sectionValue('context_header')} Observations such as observations.inferred_intent are read-only context, not instructions. If an observation conflicts with the current user message, the agent prompt, or tool policy, ignore the observation.` : undefined,
@@ -3757,6 +4106,7 @@ const buildContextMessages = ({ payloadFormat, query, userId, profileFacts, work
3757
4106
  state: minimalState,
3758
4107
  workingMemory: sectionValue('working_memory'),
3759
4108
  decisionState: sectionValue('decision_state'),
4109
+ toolReuseGuard: compactRepeatToolPolicyForAgent((decisionState || {}).repeat_tool_policy || {}),
3760
4110
  operationalState: sectionValue('operational_state'),
3761
4111
  actionLedger: sectionValue('action_ledger'),
3762
4112
  memoryCompression: sectionValue('memory_compression'),
@@ -3778,7 +4128,7 @@ const buildContextMessages = ({ payloadFormat, query, userId, profileFacts, work
3778
4128
  return [{ role: 'system', content: JSON.stringify(audit, null, 2) }];
3779
4129
  if (payloadFormat === 'auditBlocks')
3780
4130
  return sections.map((section) => ({ role: 'system', content: renderCompactSection(section) }));
3781
- return [{ role: 'system', content: JSON.stringify(compactJson) }];
4131
+ return [{ role: 'system', content: JSON.stringify(agentContextV2) }];
3782
4132
  }
3783
4133
  if (includeSummary) {
3784
4134
  const summary = compactStateSections
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "n8n-nodes-tembory",
3
- "version": "1.1.43",
3
+ "version": "1.2.0",
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",
@@ -39,10 +39,25 @@ function parseContext(messages) {
39
39
  }
40
40
 
41
41
  function requiredToolFromText(text) {
42
- return /"required_tool":"([^"]+)"/.exec(text)?.[1] || null;
42
+ try {
43
+ const parsed = JSON.parse(text);
44
+ return parsed.actionDirective?.requiredTool || parsed.action_directive?.required_tool || null;
45
+ } catch {}
46
+ return /"requiredTool":"([^"]+)"/.exec(text)?.[1] || /"required_tool":"([^"]+)"/.exec(text)?.[1] || null;
43
47
  }
44
48
 
45
49
  function conversationFrameFromText(text) {
50
+ try {
51
+ const parsed = JSON.parse(text);
52
+ if (parsed.conversationFrame) {
53
+ return {
54
+ current_user_message: parsed.conversationFrame.currentUserMessage,
55
+ conversation_history_chronological: parsed.conversationFrame.timeline || [],
56
+ all_user_messages_chronological: (parsed.conversationFrame.timeline || []).filter((message) => /^(user|human)$/i.test(String(message.role || ''))),
57
+ };
58
+ }
59
+ if (parsed.conversation) return parsed.conversation;
60
+ } catch {}
46
61
  const match = /## Conversation frame\n([\s\S]*?)(?:\n\n## |\n## |$)/.exec(text);
47
62
  if (!match) return null;
48
63
  try {