'use strict'; var chunkD4J4XPGM_cjs = require('./chunk-D4J4XPGM.cjs'); var features = require('@mastra/core/features'); var llm = require('@mastra/core/llm'); var memory = require('@mastra/core/memory'); var processors = require('@mastra/core/processors'); var xxhash = require('xxhash-wasm'); var fs = require('fs'); var path = require('path'); var crypto$1 = require('crypto'); var tokenx = require('tokenx'); var agent = require('@mastra/core/agent'); var requestContext = require('@mastra/core/request-context'); var observability = require('@mastra/core/observability'); var util = require('util'); var async_hooks = require('async_hooks'); var imageSize = require('image-size'); function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; } var xxhash__default = /*#__PURE__*/_interopDefault(xxhash); var imageSize__default = /*#__PURE__*/_interopDefault(imageSize); // src/processors/observational-memory/activation-ttl.ts var MINUTE = 6e4; var HOUR = 60 * MINUTE; var SHORT_TTL = 5 * MINUTE; var OPENAI_EXTENDED_TTL = HOUR; var GEMINI_TTL = 24 * HOUR; var DEEPSEEK_TTL = HOUR; var GROQ_TTL = 2 * HOUR; function normalize(value) { return value?.toLowerCase() ?? ""; } function isOpenAIShortTtlModel(modelId) { return /^gpt-4/.test(modelId) || /^gpt-5(?:$|-|\.([1-4])(?:$|-))/.test(modelId); } function getOpenAIPromptCacheRetention(providerOptions) { const openaiOptions = providerOptions?.openai; return typeof openaiOptions?.promptCacheRetention === "string" ? openaiOptions.promptCacheRetention.toLowerCase() : void 0; } function resolveActivationTTL(activateAfterIdle, modelContext) { if (activateAfterIdle !== "auto") { return activateAfterIdle; } return resolveAutoActivationTTL(modelContext); } function resolveAutoActivationTTL(modelContext) { const provider = normalize(modelContext?.provider); const modelId = normalize(modelContext?.modelId); if (provider.includes("openai")) { const promptCacheRetention = getOpenAIPromptCacheRetention(modelContext?.providerOptions); if (promptCacheRetention === "24h") return OPENAI_EXTENDED_TTL; if (promptCacheRetention === "in_memory") return SHORT_TTL; return isOpenAIShortTtlModel(modelId) ? SHORT_TTL : OPENAI_EXTENDED_TTL; } if (provider.includes("google") || provider.includes("gemini")) return GEMINI_TTL; if (provider.includes("deepseek")) return DEEPSEEK_TTL; if (provider.includes("groq")) return GROQ_TTL; if (provider.includes("anthropic")) return SHORT_TTL; if (provider.includes("xai") || provider.includes("grok")) return SHORT_TTL; if (provider.includes("openrouter")) return SHORT_TTL; return SHORT_TTL; } var OM_DEBUG_LOG = process.env.OM_DEBUG ? path.join(process.cwd(), "om-debug.log") : null; function omDebug(msg) { if (!OM_DEBUG_LOG) return; try { fs.appendFileSync(OM_DEBUG_LOG, `[${(/* @__PURE__ */ new Date()).toLocaleString()}] ${msg} `); } catch { } } function omError(msg, err) { const errStr = err instanceof Error ? err.stack ?? err.message : err !== void 0 ? String(err) : ""; const full = errStr ? `${msg}: ${errStr}` : msg; omDebug(`[OM:ERROR] ${full}`); } omDebug(`[OM:process-start] OM module loaded, pid=${process.pid}`); if (OM_DEBUG_LOG) { const _origConsoleError = console.error; console.error = (...args) => { omDebug( `[console.error] ${args.map((a) => { if (a instanceof Error) return a.stack ?? a.message; if (typeof a === "object" && a !== null) { try { return JSON.stringify(a); } catch { return String(a); } } return String(a); }).join(" ")}` ); _origConsoleError.apply(console, args); }; } // src/processors/observational-memory/operation-registry.ts var activeOps = /* @__PURE__ */ new Map(); function opKey(recordId, op) { return `${recordId}:${op}`; } function registerOp(recordId, op) { const key = opKey(recordId, op); activeOps.set(key, (activeOps.get(key) ?? 0) + 1); } function unregisterOp(recordId, op) { const key = opKey(recordId, op); const count = activeOps.get(key); if (!count) return; if (count <= 1) { activeOps.delete(key); } else { activeOps.set(key, count - 1); } } function isOpActiveInProcess(recordId, op) { return (activeOps.get(opKey(recordId, op)) ?? 0) > 0; } // src/processors/observational-memory/buffering-coordinator.ts var BufferingCoordinator = class _BufferingCoordinator { observationConfig; reflectionConfig; scope; /** * Track in-flight async buffering operations per resource/thread. * Key format: "obs:{lockKey}" or "refl:{lockKey}" * Value: Promise that resolves when buffering completes */ static asyncBufferingOps = /* @__PURE__ */ new Map(); /** * Track the last token boundary at which we started buffering. * Key format: "obs:{lockKey}" or "refl:{lockKey}" */ static lastBufferedBoundary = /* @__PURE__ */ new Map(); /** * Track the timestamp cursor for buffered messages. * Key format: "obs:{lockKey}" */ static lastBufferedAtTime = /* @__PURE__ */ new Map(); /** * Tracks cycleId for in-flight buffered reflections. * Key format: "refl:{lockKey}" */ static reflectionBufferCycleIds = /* @__PURE__ */ new Map(); constructor(opts) { this.observationConfig = opts.observationConfig; this.reflectionConfig = opts.reflectionConfig; this.scope = opts.scope; } getLockKey(threadId, resourceId) { if (this.scope === "resource" && resourceId) { return `resource:${resourceId}`; } return `thread:${threadId ?? "unknown"}`; } isAsyncObservationEnabled() { return this.observationConfig.bufferTokens !== void 0 && this.observationConfig.bufferTokens > 0; } isAsyncReflectionEnabled() { return this.reflectionConfig.bufferActivation !== void 0 && this.reflectionConfig.bufferActivation > 0; } getObservationBufferKey(lockKey) { return `obs:${lockKey}`; } getReflectionBufferKey(lockKey) { return `refl:${lockKey}`; } isAsyncBufferingInProgress(bufferKey) { return _BufferingCoordinator.asyncBufferingOps.has(bufferKey); } /** * Clean up static maps for a thread/resource to prevent memory leaks. */ cleanupStaticMaps(threadId, resourceId, activatedMessageIds) { const lockKey = this.getLockKey(threadId, resourceId); const obsBufKey = this.getObservationBufferKey(lockKey); const reflBufKey = this.getReflectionBufferKey(lockKey); if (activatedMessageIds) { _BufferingCoordinator.lastBufferedBoundary.delete(obsBufKey); _BufferingCoordinator.lastBufferedAtTime.delete(obsBufKey); } else { _BufferingCoordinator.lastBufferedAtTime.delete(obsBufKey); _BufferingCoordinator.lastBufferedBoundary.delete(obsBufKey); _BufferingCoordinator.lastBufferedBoundary.delete(reflBufKey); _BufferingCoordinator.asyncBufferingOps.delete(obsBufKey); _BufferingCoordinator.asyncBufferingOps.delete(reflBufKey); _BufferingCoordinator.reflectionBufferCycleIds.delete(reflBufKey); } } /** * Check if we've crossed a new bufferTokens interval boundary for async observation. */ shouldTriggerAsyncObservation(currentTokens, lockKey, record, storage, messageTokensThreshold) { if (!this.isAsyncObservationEnabled()) return false; if (record.isBufferingObservation) { if (isOpActiveInProcess(record.id, "bufferingObservation")) return false; omDebug(`[OM:shouldTriggerAsyncObs] isBufferingObservation=true but stale, clearing`); storage?.setBufferingObservationFlag(record.id, false)?.catch(() => { }); } const bufferKey = this.getObservationBufferKey(lockKey); if (this.isAsyncBufferingInProgress(bufferKey)) return false; const bufferTokens = this.observationConfig.bufferTokens; const dbBoundary = record.lastBufferedAtTokens ?? 0; const memBoundary = _BufferingCoordinator.lastBufferedBoundary.get(bufferKey) ?? 0; const lastBoundary = Math.max(dbBoundary, memBoundary); const rampPoint = messageTokensThreshold ? messageTokensThreshold - bufferTokens * 1.1 : Infinity; const effectiveBufferTokens = currentTokens >= rampPoint ? bufferTokens / 2 : bufferTokens; const currentInterval = Math.floor(currentTokens / effectiveBufferTokens); const lastInterval = Math.floor(lastBoundary / effectiveBufferTokens); const shouldTrigger = currentInterval > lastInterval; omDebug( `[OM:shouldTriggerAsyncObs] tokens=${currentTokens}, bufferTokens=${bufferTokens}, effectiveBufferTokens=${effectiveBufferTokens}, rampPoint=${rampPoint}, currentInterval=${currentInterval}, lastInterval=${lastInterval}, lastBoundary=${lastBoundary} (db=${dbBoundary}, mem=${memBoundary}), shouldTrigger=${shouldTrigger}` ); return shouldTrigger; } /** * Await any in-flight async buffering operations for a given thread/resource. */ static async awaitBuffering(threadId, resourceId, scope, timeoutMs = 3e4) { const lockKey = scope === "resource" && resourceId ? `resource:${resourceId}` : `thread:${threadId ?? "unknown"}`; const obsKey = `obs:${lockKey}`; const reflKey = `refl:${lockKey}`; const promises = []; const obsOp = _BufferingCoordinator.asyncBufferingOps.get(obsKey); if (obsOp) promises.push(obsOp); const reflOp = _BufferingCoordinator.asyncBufferingOps.get(reflKey); if (reflOp) promises.push(reflOp); if (promises.length === 0) { return; } let timeoutId; try { await Promise.race([ Promise.allSettled(promises).then(() => void 0), new Promise((resolve) => { timeoutId = setTimeout(resolve, timeoutMs); }) ]); } finally { if (timeoutId !== void 0) { clearTimeout(timeoutId); } } } }; // src/processors/observational-memory/model-context.ts function parseComparableModelContext(model) { const slashIndex = model.indexOf("/"); if (slashIndex === -1) return { modelId: model }; return { provider: model.slice(0, slashIndex).split(".")[0], modelId: model.slice(slashIndex + 1) }; } function didProviderChange(actorModel, lastModel) { if (actorModel === void 0 || lastModel === void 0) return false; const actorContext = parseComparableModelContext(actorModel); const lastContext = parseComparableModelContext(lastModel); if (actorContext.modelId !== lastContext.modelId) return true; if (actorContext.provider && lastContext.provider) { return actorContext.provider !== lastContext.provider; } return false; } // src/processors/observational-memory/date-utils.ts function formatRelativeTime(date, currentDate) { const diffMs = currentDate.getTime() - date.getTime(); const diffDays = Math.floor(diffMs / (1e3 * 60 * 60 * 24)); if (diffDays < 0) { const futureDays = Math.abs(diffDays); if (futureDays === 1) return "tomorrow"; if (futureDays < 7) return `in ${futureDays} days`; if (futureDays < 14) return "in 1 week"; if (futureDays < 30) return `in ${Math.floor(futureDays / 7)} weeks`; if (futureDays < 60) return "in 1 month"; if (futureDays < 365) return `in ${Math.floor(futureDays / 30)} months`; const years = Math.floor(futureDays / 365); return `in ${years} year${years > 1 ? "s" : ""}`; } if (diffDays === 0) return "today"; if (diffDays === 1) return "yesterday"; if (diffDays < 7) return `${diffDays} days ago`; if (diffDays < 14) return "1 week ago"; if (diffDays < 30) return `${Math.floor(diffDays / 7)} weeks ago`; if (diffDays < 60) return "1 month ago"; if (diffDays < 365) return `${Math.floor(diffDays / 30)} months ago`; return `${Math.floor(diffDays / 365)} year${Math.floor(diffDays / 365) > 1 ? "s" : ""} ago`; } function formatGapBetweenDates(prevDate, currDate) { const diffMs = currDate.getTime() - prevDate.getTime(); const diffDays = Math.floor(diffMs / (1e3 * 60 * 60 * 24)); if (diffDays <= 1) { return null; } else if (diffDays < 7) { return `[${diffDays} days later]`; } else if (diffDays < 14) { return `[1 week later]`; } else if (diffDays < 30) { const weeks = Math.floor(diffDays / 7); return `[${weeks} weeks later]`; } else if (diffDays < 60) { return `[1 month later]`; } else { const months = Math.floor(diffDays / 30); return `[${months} months later]`; } } function parseDateFromContent(dateContent) { let targetDate = null; const simpleDateMatch = dateContent.match(/([A-Z][a-z]+)\s+(\d{1,2}),?\s+(\d{4})/); if (simpleDateMatch) { const parsed = /* @__PURE__ */ new Date(`${simpleDateMatch[1]} ${simpleDateMatch[2]}, ${simpleDateMatch[3]}`); if (!isNaN(parsed.getTime())) { targetDate = parsed; } } if (!targetDate) { const rangeMatch = dateContent.match(/([A-Z][a-z]+)\s+(\d{1,2})-\d{1,2},?\s+(\d{4})/); if (rangeMatch) { const parsed = /* @__PURE__ */ new Date(`${rangeMatch[1]} ${rangeMatch[2]}, ${rangeMatch[3]}`); if (!isNaN(parsed.getTime())) { targetDate = parsed; } } } if (!targetDate) { const vagueMatch = dateContent.match( /(late|early|mid)[- ]?(?:to[- ]?(?:late|early|mid)[- ]?)?([A-Z][a-z]+)\s+(\d{4})/i ); if (vagueMatch) { const month = vagueMatch[2]; const year = vagueMatch[3]; const modifier = vagueMatch[1].toLowerCase(); let day = 15; if (modifier === "early") day = 7; if (modifier === "late") day = 23; const parsed = /* @__PURE__ */ new Date(`${month} ${day}, ${year}`); if (!isNaN(parsed.getTime())) { targetDate = parsed; } } } if (!targetDate) { const crossMonthMatch = dateContent.match(/([A-Z][a-z]+)\s+to\s+(?:early\s+)?([A-Z][a-z]+)\s+(\d{4})/i); if (crossMonthMatch) { const parsed = /* @__PURE__ */ new Date(`${crossMonthMatch[2]} 1, ${crossMonthMatch[3]}`); if (!isNaN(parsed.getTime())) { targetDate = parsed; } } } return targetDate; } function isFutureIntentObservation(line) { const futureIntentPatterns = [ /\bwill\s+(?:be\s+)?(?:\w+ing|\w+)\b/i, /\bplans?\s+to\b/i, /\bplanning\s+to\b/i, /\blooking\s+forward\s+to\b/i, /\bgoing\s+to\b/i, /\bintends?\s+to\b/i, /\bwants?\s+to\b/i, /\bneeds?\s+to\b/i, /\babout\s+to\b/i ]; return futureIntentPatterns.some((pattern) => pattern.test(line)); } function expandInlineEstimatedDates(observations, currentDate) { const inlineDateRegex = /\((estimated|meaning)\s+([^)]+\d{4})\)/gi; return observations.replace(inlineDateRegex, (match, prefix, dateContent, offset) => { const targetDate = parseDateFromContent(dateContent); if (targetDate) { const relative = formatRelativeTime(targetDate, currentDate); const lineStart = observations.lastIndexOf("\n", offset) + 1; const lineBeforeDate = observations.slice(lineStart, offset); const isPastDate = targetDate < currentDate; const isFutureIntent = isFutureIntentObservation(lineBeforeDate); if (isPastDate && isFutureIntent) { return `(${prefix} ${dateContent} - ${relative}, likely already happened)`; } return `(${prefix} ${dateContent} - ${relative})`; } return match; }); } function addRelativeTimeToObservations(observations, currentDate) { const withInlineDates = expandInlineEstimatedDates(observations, currentDate); const dateHeaderRegex = /^(Date:\s*)([A-Z][a-z]+ \d{1,2}, \d{4})$/gm; const dates = []; let regexMatch; while ((regexMatch = dateHeaderRegex.exec(withInlineDates)) !== null) { const dateStr = regexMatch[2]; const parsed = new Date(dateStr); if (!isNaN(parsed.getTime())) { dates.push({ index: regexMatch.index, date: parsed, match: regexMatch[0], prefix: regexMatch[1], dateStr }); } } if (dates.length === 0) { return withInlineDates; } let result = ""; let lastIndex = 0; for (let i = 0; i < dates.length; i++) { const curr = dates[i]; const prev = i > 0 ? dates[i - 1] : null; result += withInlineDates.slice(lastIndex, curr.index); if (prev) { const gap = formatGapBetweenDates(prev.date, curr.date); if (gap) { result += ` ${gap} `; } } const relative = formatRelativeTime(curr.date, currentDate); result += `${curr.prefix}${curr.dateStr} (${relative})`; lastIndex = curr.index + curr.match.length; } result += withInlineDates.slice(lastIndex); return result; } var MIN_TEMPORAL_GAP_MS = 10 * 60 * 1e3; function formatTemporalGap(diffMs) { if (diffMs < MIN_TEMPORAL_GAP_MS) return null; const minute = 60 * 1e3; const hour = 60 * minute; const day = 24 * hour; const week = 7 * day; const month = 30 * day; const year = 365 * day; const formatUnit = (value, unit) => `${value} ${unit}${value === 1 ? "" : "s"}`; if (diffMs < hour) { const minutes = Math.max(1, Math.round(diffMs / minute)); return `${formatUnit(minutes, "minute")} later`; } const formatTwoUnits = (primaryMs, primaryUnit, secondaryMs, secondaryUnit) => { const primary = Math.floor(diffMs / primaryMs); const remainder = diffMs - primary * primaryMs; const secondary = Math.floor(remainder / secondaryMs); const parts = [formatUnit(primary, primaryUnit)]; if (secondary > 0) { parts.push(formatUnit(secondary, secondaryUnit)); } return `${parts.join(" ")} later`; }; if (diffMs < day) { return formatTwoUnits(hour, "hour", minute, "minute"); } if (diffMs < week) { return formatTwoUnits(day, "day", hour, "hour"); } if (diffMs < month) { return formatTwoUnits(week, "week", day, "day"); } if (diffMs < year) { return formatTwoUnits(month, "month", week, "week"); } return formatTwoUnits(year, "year", month, "month"); } function formatTemporalTimestamp(date) { return date.toLocaleString("en-US", { year: "numeric", month: "2-digit", day: "2-digit", hour: "numeric", minute: "2-digit", hour12: true, timeZoneName: "short" }); } function getMessagePartTimestamp(msg, position) { const timestamps = msg.content?.parts?.map((part) => "createdAt" in part ? part.createdAt : void 0).filter((timestamp) => typeof timestamp === "number"); if (timestamps && timestamps.length > 0) { const index = position === "first" ? 0 : timestamps.length - 1; const timestamp = timestamps[index]; if (timestamp !== void 0) return timestamp; } return new Date(msg.createdAt).getTime(); } function isTemporalGapMarker(msg) { return msg.id.startsWith("__temporal_"); } // src/processors/observational-memory/markers.ts function createObservationStartMarker(params) { return { type: "data-om-observation-start", data: { cycleId: params.cycleId, operationType: params.operationType, startedAt: (/* @__PURE__ */ new Date()).toISOString(), tokensToObserve: params.tokensToObserve, recordId: params.recordId, threadId: params.threadId, threadIds: params.threadIds, config: params.config } }; } function createObservationEndMarker(params) { const completedAt = (/* @__PURE__ */ new Date()).toISOString(); const durationMs = new Date(completedAt).getTime() - new Date(params.startedAt).getTime(); return { type: "data-om-observation-end", data: { cycleId: params.cycleId, operationType: params.operationType, completedAt, durationMs, tokensObserved: params.tokensObserved, observationTokens: params.observationTokens, observations: params.observations, currentTask: params.currentTask, suggestedResponse: params.suggestedResponse, recordId: params.recordId, threadId: params.threadId } }; } function createObservationFailedMarker(params) { const failedAt = (/* @__PURE__ */ new Date()).toISOString(); const durationMs = new Date(failedAt).getTime() - new Date(params.startedAt).getTime(); return { type: "data-om-observation-failed", data: { cycleId: params.cycleId, operationType: params.operationType, failedAt, durationMs, tokensAttempted: params.tokensAttempted, error: params.error, recordId: params.recordId, threadId: params.threadId } }; } function createBufferingStartMarker(params) { return { type: "data-om-buffering-start", data: { cycleId: params.cycleId, operationType: params.operationType, startedAt: (/* @__PURE__ */ new Date()).toISOString(), tokensToBuffer: params.tokensToBuffer, recordId: params.recordId, threadId: params.threadId, threadIds: params.threadIds, config: params.config } }; } function createBufferingEndMarker(params) { const completedAt = (/* @__PURE__ */ new Date()).toISOString(); const durationMs = new Date(completedAt).getTime() - new Date(params.startedAt).getTime(); return { type: "data-om-buffering-end", data: { cycleId: params.cycleId, operationType: params.operationType, completedAt, durationMs, tokensBuffered: params.tokensBuffered, bufferedTokens: params.bufferedTokens, recordId: params.recordId, threadId: params.threadId, observations: params.observations } }; } function createBufferingFailedMarker(params) { const failedAt = (/* @__PURE__ */ new Date()).toISOString(); const durationMs = new Date(failedAt).getTime() - new Date(params.startedAt).getTime(); return { type: "data-om-buffering-failed", data: { cycleId: params.cycleId, operationType: params.operationType, failedAt, durationMs, tokensAttempted: params.tokensAttempted, error: params.error, recordId: params.recordId, threadId: params.threadId } }; } function createActivationMarker(params) { return { type: "data-om-activation", data: { cycleId: params.cycleId, operationType: params.operationType, activatedAt: (/* @__PURE__ */ new Date()).toISOString(), chunksActivated: params.chunksActivated, tokensActivated: params.tokensActivated, observationTokens: params.observationTokens, messagesActivated: params.messagesActivated, recordId: params.recordId, threadId: params.threadId, generationCount: params.generationCount, config: params.config, observations: params.observations, triggeredBy: params.triggeredBy, lastActivityAt: params.lastActivityAt, ttlExpiredMs: params.ttlExpiredMs, previousModel: params.previousModel, currentModel: params.currentModel } }; } function createThreadUpdateMarker(params) { return { type: "data-om-thread-update", data: { cycleId: params.cycleId, threadId: params.threadId, oldTitle: params.oldTitle, newTitle: params.newTitle, timestamp: (/* @__PURE__ */ new Date()).toISOString() } }; } // src/processors/observational-memory/message-utils.ts function findLastCompletedObservationBoundary(message) { const parts = message.content?.parts; if (!parts || !Array.isArray(parts)) return -1; for (let i = parts.length - 1; i >= 0; i--) { const part = parts[i]; if (part?.type === "data-om-observation-end") { return i; } } return -1; } function getUnobservedParts(message) { const parts = message.content?.parts; if (!parts || !Array.isArray(parts)) return []; const endMarkerIndex = findLastCompletedObservationBoundary(message); if (endMarkerIndex === -1) { return parts.filter((p) => { const part = p; return part?.type !== "data-om-observation-start"; }); } return parts.slice(endMarkerIndex + 1).filter((p) => { const part = p; return !part?.type?.startsWith("data-om-observation-"); }); } function getLastObservedMessageCursor(messages) { let latest; for (const msg of messages) { if (!msg?.id || !msg.createdAt) continue; if (!latest || new Date(msg.createdAt).getTime() > new Date(latest.createdAt).getTime()) { latest = msg; } } return latest ? { createdAt: new Date(latest.createdAt).toISOString(), id: latest.id } : void 0; } function isMessageAtOrBeforeCursor(msg, cursor) { if (!msg.createdAt) return false; const msgIso = new Date(msg.createdAt).toISOString(); if (msgIso < cursor.createdAt) return true; if (msgIso === cursor.createdAt && msg.id === cursor.id) return true; return false; } function filterObservedMessages(opts) { const { messageList, record } = opts; const allMessages = messageList.get.all.db(); const useMarkerBoundaryPruning = opts.useMarkerBoundaryPruning ?? true; const preserveMessageIds = opts.preserveMessageIds ?? /* @__PURE__ */ new Set(); let markerMessageIndex = -1; let markerMessage = null; for (let i = allMessages.length - 1; i >= 0; i--) { const msg = allMessages[i]; if (!msg) continue; if (findLastCompletedObservationBoundary(msg) !== -1) { markerMessageIndex = i; markerMessage = msg; break; } } if (useMarkerBoundaryPruning && markerMessage && markerMessageIndex !== -1) { const messagesToRemove = []; for (let i = 0; i < markerMessageIndex; i++) { const msg = allMessages[i]; if (msg?.id && msg.id !== "om-continuation" && !preserveMessageIds.has(msg.id)) { messagesToRemove.push(msg.id); } } if (messagesToRemove.length > 0) { messageList.removeByIds(messagesToRemove); } const unobserved = getUnobservedParts(markerMessage); if (unobserved.length === 0) { if (markerMessage.id) messageList.removeByIds([markerMessage.id]); } else if (unobserved.length < (markerMessage.content?.parts?.length ?? 0)) { markerMessage.content.parts = unobserved; } } else if (record) { const observedIds = new Set(Array.isArray(record.observedMessageIds) ? record.observedMessageIds : []); const derivedCursor = opts.fallbackCursor ?? getLastObservedMessageCursor(allMessages.filter((msg) => !!msg?.id && observedIds.has(msg.id) && !!msg.createdAt)); const lastObservedAt = record.lastObservedAt; const messagesToRemove = []; for (const msg of allMessages) { if (!msg?.id || msg.id === "om-continuation" || preserveMessageIds.has(msg.id)) continue; if (observedIds.has(msg.id)) { messagesToRemove.push(msg.id); continue; } if (derivedCursor && isMessageAtOrBeforeCursor(msg, derivedCursor)) { messagesToRemove.push(msg.id); continue; } if (lastObservedAt && msg.createdAt) { const msgDate = new Date(msg.createdAt); if (msgDate <= lastObservedAt) { messagesToRemove.push(msg.id); } } } if (messagesToRemove.length > 0) { messageList.removeByIds(messagesToRemove); } } } function getBufferedChunks(record) { if (!record?.bufferedObservationChunks) return []; if (Array.isArray(record.bufferedObservationChunks)) return record.bufferedObservationChunks; if (typeof record.bufferedObservationChunks === "string") { try { const parsed = JSON.parse(record.bufferedObservationChunks); return Array.isArray(parsed) ? parsed : []; } catch { return []; } } return []; } function combineObservationsForBuffering(activeObservations, bufferedObservations) { if (!activeObservations && !bufferedObservations) return void 0; if (!activeObservations) return bufferedObservations; if (!bufferedObservations) return activeObservations; return `${activeObservations} --- BUFFERED (pending activation) --- ${bufferedObservations}`; } function sortThreadsByOldestMessage(messagesByThread) { return Array.from(messagesByThread.entries()).map(([threadId, messages]) => ({ threadId, oldestTimestamp: Math.min(...messages.map((m) => m.createdAt ? new Date(m.createdAt).getTime() : Date.now())) })).sort((a, b) => a.oldestTimestamp - b.oldestTimestamp).map((t) => t.threadId); } function stripThreadTags(observations) { return observations.replace(/<\/?thread\b[^>]{0,1024}>/gi, "").trim(); } // src/processors/observational-memory/model-by-input-tokens.ts function isTieredModelTarget(model) { if (typeof model === "string") { return true; } if (!model || typeof model !== "object") { return false; } return "modelId" in model || "id" in model || "providerId" in model || "provider" in model || "doGenerate" in model && "doStream" in model; } function normalizeThresholds(config) { const entries = Object.entries(config.upTo); if (entries.length === 0) { throw new Error('ModelByInputTokens requires at least one threshold in "upTo"'); } for (const [limitStr, model] of entries) { const limit = Number(limitStr); if (!Number.isFinite(limit) || limit <= 0) { throw new Error(`ModelByInputTokens threshold keys must be positive numbers. Got: ${limitStr}`); } if (!isTieredModelTarget(model)) { throw new Error(`ModelByInputTokens requires a valid model target for threshold ${limitStr}`); } } return entries.map(([limitStr, model]) => ({ limit: Number(limitStr), model })).sort((a, b) => a.limit - b.limit); } var ModelByInputTokens = class { thresholds; constructor(config) { this.thresholds = normalizeThresholds(config); } resolve(inputTokens) { for (const { limit, model } of this.thresholds) { if (inputTokens <= limit) { return model; } } const maxLimit = this.thresholds[this.thresholds.length - 1].limit; throw new Error( `ModelByInputTokens: input token count (${inputTokens}) exceeds the largest configured threshold (${maxLimit}). Please configure a higher threshold or use a larger model.` ); } getThresholds() { return this.thresholds.map((t) => t.limit); } }; var OBSERVATION_GROUP_PATTERN = /]*)>([\s\S]*?)<\/observation-group>/g; var ATTRIBUTE_PATTERN = /([\w][\w-]*)="([^"]*)"/g; var REFLECTION_GROUP_SPLIT_PATTERN = /^##\s+Group\s+/m; function parseObservationGroupAttributes(attributeString) { const attributes = {}; for (const match of attributeString.matchAll(ATTRIBUTE_PATTERN)) { const [, key, value] = match; if (key && value !== void 0) { attributes[key] = value; } } return attributes; } function parseReflectionObservationGroupSections(content) { const normalizedContent = content.trim(); if (!normalizedContent || !REFLECTION_GROUP_SPLIT_PATTERN.test(normalizedContent)) { return []; } return normalizedContent.split(REFLECTION_GROUP_SPLIT_PATTERN).map((section) => section.trim()).filter(Boolean).map((section) => { const newlineIndex = section.indexOf("\n"); const heading = (newlineIndex >= 0 ? section.slice(0, newlineIndex) : section).trim(); const body = (newlineIndex >= 0 ? section.slice(newlineIndex + 1) : "").trim(); return { heading, body: stripReflectionGroupMetadata(body) }; }); } function stripReflectionGroupMetadata(body) { return body.replace(/^_range:\s*`[^`]*`_\s*\n?/m, "").trim(); } function generateAnchorId() { return crypto$1.randomBytes(8).toString("hex"); } function wrapInObservationGroup(observations, range, id = generateAnchorId(), _sourceGroupIds, kind) { const content = observations.trim(); const kindAttr = kind ? ` kind="${kind}"` : ""; return ` ${content} `; } function parseObservationGroups(observations) { if (!observations) { return []; } const groups = []; let match; while ((match = OBSERVATION_GROUP_PATTERN.exec(observations)) !== null) { const attributes = parseObservationGroupAttributes(match[1] ?? ""); const id = attributes.id; const range = attributes.range; if (!id || !range) { continue; } groups.push({ id, range, kind: attributes.kind, content: match[2].trim() }); } return groups; } function stripObservationGroups(observations) { if (!observations) { return observations; } return observations.replace(OBSERVATION_GROUP_PATTERN, (_match, _attributes, content) => content.trim()).replace(/\n{3,}/g, "\n\n").trim(); } function getRangeSegments(range) { return range.split(",").map((segment) => segment.trim()).filter(Boolean); } function combineObservationGroupRanges(groups) { const segments = groups.flatMap((group) => getRangeSegments(group.range)); if (segments.length === 0) { return ""; } const firstSegment = segments[0]; const lastSegment = segments[segments.length - 1]; const firstStart = firstSegment?.split(":")[0]?.trim(); const lastEnd = lastSegment?.split(":").at(-1)?.trim(); if (firstStart && lastEnd) { return `${firstStart}:${lastEnd}`; } return Array.from(new Set(segments)).join(","); } function renderObservationGroupsForReflection(observations) { const groups = parseObservationGroups(observations); if (groups.length === 0) { return null; } const groupsByContent = new Map(groups.map((g) => [g.content.trim(), g])); const result = observations.replace(OBSERVATION_GROUP_PATTERN, (_match, _attrs, content) => { const group = groupsByContent.get(content.trim()); if (!group) return content.trim(); return `## Group \`${group.id}\` _range: \`${group.range}\`_ ${group.content}`; }); return result.replace(/\n{3,}/g, "\n\n").trim(); } function getCanonicalGroupId(sectionHeading, fallbackIndex) { const match = sectionHeading.match(/`([^`]+)`/); return match?.[1]?.trim() || `derived-group-${fallbackIndex + 1}`; } function deriveObservationGroupProvenance(content, groups) { const sections = parseReflectionObservationGroupSections(content); if (sections.length === 0 || groups.length === 0) { return []; } return sections.map((section, index) => { const bodyLines = new Set( section.body.split("\n").map((line) => line.trim()).filter(Boolean) ); const matchingGroups = groups.filter((group) => { const groupLines = group.content.split("\n").map((line) => line.trim()).filter(Boolean); return groupLines.some((line) => bodyLines.has(line)); }); const fallbackGroup = groups[Math.min(index, groups.length - 1)]; const resolvedGroups = matchingGroups.length > 0 ? matchingGroups : fallbackGroup ? [fallbackGroup] : []; const canonicalGroupId = getCanonicalGroupId(section.heading, index); return { id: canonicalGroupId, range: combineObservationGroupRanges(resolvedGroups), kind: "reflection", content: section.body }; }); } function reconcileObservationGroupsFromReflection(content, sourceObservations) { const sourceGroups = parseObservationGroups(sourceObservations); if (sourceGroups.length === 0) { return null; } const normalizedContent = content.trim(); if (!normalizedContent) { return ""; } const derivedGroups = deriveObservationGroupProvenance(normalizedContent, sourceGroups); if (derivedGroups.length > 0) { return derivedGroups.map((group) => wrapInObservationGroup(group.content, group.range, group.id, void 0, group.kind)).join("\n\n"); } return wrapInObservationGroup( normalizedContent, combineObservationGroupRanges(sourceGroups), generateAnchorId(), void 0, "reflection" ); } // src/processors/observational-memory/thresholds.ts function getMaxThreshold(threshold) { if (typeof threshold === "number") { return threshold; } return threshold.max; } function calculateDynamicThreshold(threshold, currentObservationTokens) { if (typeof threshold === "number") { return threshold; } const totalBudget = threshold.max; const baseThreshold = threshold.min; const effectiveThreshold = Math.max(totalBudget - currentObservationTokens, baseThreshold); return Math.round(effectiveThreshold); } function resolveBufferTokens(bufferTokens, messageTokens) { if (bufferTokens === false) return void 0; if (bufferTokens === void 0) return void 0; if (bufferTokens > 0 && bufferTokens < 1) { return Math.round(getMaxThreshold(messageTokens) * bufferTokens); } return bufferTokens; } function resolveBlockAfter(blockAfter, messageTokens) { if (blockAfter === void 0) return void 0; if (blockAfter >= 1 && blockAfter < 100) { return Math.round(getMaxThreshold(messageTokens) * blockAfter); } return blockAfter; } function resolveRetentionFloor(bufferActivation, messageTokensThreshold) { if (bufferActivation >= 1e3) return bufferActivation; const ratio = Math.max(0, Math.min(1, bufferActivation)); return messageTokensThreshold * (1 - ratio); } function resolveActivationRatio(bufferActivation, messageTokensThreshold) { if (bufferActivation >= 1e3) { return Math.max(0, Math.min(1, 1 - bufferActivation / messageTokensThreshold)); } return Math.max(0, Math.min(1, bufferActivation)); } function calculateProjectedMessageRemoval(chunks, bufferActivation, messageTokensThreshold, currentPendingTokens) { if (chunks.length === 0) return 0; const retentionFloor = resolveRetentionFloor(bufferActivation, messageTokensThreshold); const targetMessageTokens = Math.max(0, currentPendingTokens - retentionFloor); if (targetMessageTokens === 0) return 0; let cumulativeMessageTokens = 0; let bestOverBoundary = 0; let bestOverTokens = 0; let bestUnderBoundary = 0; let bestUnderTokens = 0; for (let i = 0; i < chunks.length; i++) { cumulativeMessageTokens += chunks[i].messageTokens ?? 0; const boundary = i + 1; if (cumulativeMessageTokens >= targetMessageTokens) { if (bestOverBoundary === 0 || cumulativeMessageTokens < bestOverTokens) { bestOverBoundary = boundary; bestOverTokens = cumulativeMessageTokens; } } else { if (cumulativeMessageTokens > bestUnderTokens) { bestUnderBoundary = boundary; bestUnderTokens = cumulativeMessageTokens; } } } const maxOvershoot = retentionFloor * 0.95; const overshoot = bestOverTokens - targetMessageTokens; const remainingAfterOver = currentPendingTokens - bestOverTokens; const remainingAfterUnder = currentPendingTokens - bestUnderTokens; const minRemaining = Math.min(1e3, retentionFloor); let bestBoundaryMessageTokens; if (bestOverBoundary > 0 && overshoot <= maxOvershoot && remainingAfterOver >= minRemaining) { bestBoundaryMessageTokens = bestOverTokens; } else if (bestUnderBoundary > 0 && remainingAfterUnder >= minRemaining) { bestBoundaryMessageTokens = bestUnderTokens; } else if (bestOverBoundary > 0) { bestBoundaryMessageTokens = bestOverTokens; } else { return chunks[0]?.messageTokens ?? 0; } return bestBoundaryMessageTokens; } // src/processors/observational-memory/observation-strategies/base.ts var hasherPromise = xxhash__default.default(); var ObservationStrategy = class _ObservationStrategy { constructor(deps, opts) { this.deps = deps; this.opts = opts; this.storage = deps.storage; this.messageHistory = deps.messageHistory; this.tokenCounter = deps.tokenCounter; this.observationConfig = deps.observationConfig; this.reflectionConfig = deps.reflectionConfig; this.scope = deps.scope; this.retrieval = deps.retrieval; } deps; opts; storage; messageHistory; tokenCounter; observationConfig; reflectionConfig; scope; retrieval; /** Select the right strategy based on scope and mode. Wired up by index.ts. */ static create; /** * Run the full observation lifecycle. * @returns Result with `observed` flag and optional `usage` from the observer LLM call. * @throws On sync/resource-scoped observer failure after failed markers (same as pre–Option-A contract). */ async run() { const { record, threadId, abortSignal, writer, reflectionHooks, requestContext } = this.opts; const cycleId = this.generateCycleId(); try { if (this.needsLock) { const fresh = await this.storage.getObservationalMemory(record.threadId, record.resourceId); if (fresh?.lastObservedAt && record.lastObservedAt && fresh.lastObservedAt > record.lastObservedAt) { return { observed: false }; } } const { messages, existingObservations } = await this.prepare(); await this.emitStartMarkers(cycleId); const output = await this.observe(existingObservations, messages); const processed = await this.process(output, existingObservations); await this.persist(processed); await this.emitEndMarkers(cycleId, processed); if (this.needsReflection) { await this.deps.reflector.maybeReflect({ record: { ...record, activeObservations: processed.observations }, observationTokens: processed.observationTokens, threadId, writer, abortSignal, reflectionHooks, requestContext, observabilityContext: this.opts.observabilityContext }); } return { observed: true, usage: output.usage }; } catch (error) { await this.emitFailedMarkers(cycleId, error); if (!this.rethrowOnFailure) { const failedMarkerForStorage = { type: "data-om-observation-failed", data: { cycleId, operationType: "observation", startedAt: (/* @__PURE__ */ new Date()).toISOString(), error: error instanceof Error ? error.message : String(error), recordId: record.id, threadId } }; await this.persistMarkerToStorage(failedMarkerForStorage, threadId, this.opts.resourceId).catch(() => { }); if (abortSignal?.aborted) throw error; omError("[OM] Observation failed", error); return { observed: false }; } omError("[OM] Observation failed", error); throw error; } } // ── Shared helpers ────────────────────────────────────────── generateCycleId() { return crypto.randomUUID(); } async streamMarker(marker) { if (this.opts.writer) { await this.opts.writer.custom({ ...marker, transient: true }).catch(() => { }); } const markerThreadId = marker.data?.threadId ?? this.opts.threadId; await this.persistMarkerToStorage(marker, markerThreadId, this.opts.resourceId); } getObservationMarkerConfig() { return { messageTokens: getMaxThreshold(this.observationConfig.messageTokens), observationTokens: getMaxThreshold(this.reflectionConfig.observationTokens), scope: this.scope }; } getMaxMessageTimestamp(messages) { let maxTime = 0; for (const msg of messages) { if (msg.createdAt) { const msgTime = new Date(msg.createdAt).getTime(); if (msgTime > maxTime) { maxTime = msgTime; } } } return maxTime > 0 ? new Date(maxTime) : /* @__PURE__ */ new Date(); } // ── Observation formatting ────────────────────────────────── /** * Wrap observations in a thread attribution tag. * In resource scope, thread IDs can be obscured via xxhash. */ async wrapWithThreadTag(threadId, observations, messageRange) { const cleanObservations = stripThreadTags(observations); const groupedObservations = this.retrieval && messageRange ? wrapInObservationGroup(cleanObservations, messageRange) : cleanObservations; let displayId = threadId; if (this.deps.obscureThreadIds) { const hasher = await hasherPromise; displayId = hasher.h32ToString(threadId); } return ` ${groupedObservations} `; } /** * Create a message boundary delimiter with an ISO 8601 date. * Used to separate observation chunks for cache stability. */ static createMessageBoundary(date) { return ` --- message boundary (${date.toISOString()}) --- `; } /** * Wrap raw observations — in resource scope, wraps with thread tag and merges; * in thread scope, simply appends with a message boundary delimiter. */ wrapObservations(rawObservations, existingObservations, threadId, lastObservedAt, messageRange) { if (this.scope === "resource") { return (async () => { const threadSection = await this.wrapWithThreadTag(threadId, rawObservations, messageRange); return this.replaceOrAppendThreadSection(existingObservations, threadId, threadSection, lastObservedAt); })(); } const grouped = this.retrieval && messageRange ? wrapInObservationGroup(rawObservations, messageRange) : rawObservations; if (!existingObservations) return grouped; const boundary = lastObservedAt ? _ObservationStrategy.createMessageBoundary(lastObservedAt) : "\n\n"; return `${existingObservations}${boundary}${grouped}`; } replaceOrAppendThreadSection(existingObservations, _threadId, newThreadSection, lastObservedAt) { if (!existingObservations) { return newThreadSection; } const threadIdMatch = newThreadSection.match(//); const dateMatch = newThreadSection.match(/Date:\s*([A-Za-z]+\s+\d+,\s+\d+)/); if (!threadIdMatch || !dateMatch) { const boundary2 = lastObservedAt ? _ObservationStrategy.createMessageBoundary(lastObservedAt) : "\n\n"; return `${existingObservations}${boundary2}${newThreadSection}`; } const newThreadId = threadIdMatch[1]; const newDate = dateMatch[1]; const threadOpen = ``; const threadClose = ""; const startIdx = existingObservations.indexOf(threadOpen); let existingSection = null; let existingSectionStart = -1; let existingSectionEnd = -1; if (startIdx !== -1) { const closeIdx = existingObservations.indexOf(threadClose, startIdx); if (closeIdx !== -1) { existingSectionEnd = closeIdx + threadClose.length; existingSectionStart = startIdx; const section = existingObservations.slice(startIdx, existingSectionEnd); if (section.includes(`Date: ${newDate}`) || section.includes(`Date:${newDate}`)) { existingSection = section; } } } if (existingSection) { const dateLineEnd = newThreadSection.indexOf("\n", newThreadSection.indexOf("Date:")); const newCloseIdx = newThreadSection.lastIndexOf(threadClose); if (dateLineEnd !== -1 && newCloseIdx !== -1) { const newObsContent = newThreadSection.slice(dateLineEnd + 1, newCloseIdx).trim(); if (newObsContent) { const withoutClose = existingSection.slice(0, existingSection.length - threadClose.length).trimEnd(); const merged = `${withoutClose} ${newObsContent} ${threadClose}`; return existingObservations.slice(0, existingSectionStart) + merged + existingObservations.slice(existingSectionEnd); } } } const boundary = lastObservedAt ? _ObservationStrategy.createMessageBoundary(lastObservedAt) : "\n\n"; return `${existingObservations}${boundary}${newThreadSection}`; } async indexObservationGroups(observations, threadId, resourceId, observedAt) { if (!resourceId || !this.deps.onIndexObservations) { return; } const groups = parseObservationGroups(observations); if (groups.length === 0) { return; } await Promise.all( groups.map( (group) => this.deps.onIndexObservations({ text: group.content, groupId: group.id, range: group.range, threadId, resourceId, observedAt }) ) ); } // ── Marker persistence ────────────────────────────────────── /** * Persist a marker to the last assistant message in storage. * Fetches messages directly from the DB so it works even when * no MessageList is available (e.g. async buffering ops). */ async persistMarkerToStorage(marker, threadId, resourceId) { try { const result = await this.storage.listMessages({ threadId, perPage: 20, orderBy: { field: "createdAt", direction: "DESC" } }); const messages = result?.messages ?? []; for (const msg of messages) { if (msg?.role === "assistant" && msg.content?.parts && Array.isArray(msg.content.parts)) { const markerData = marker.data; const alreadyPresent = markerData?.cycleId && msg.content.parts.some((p) => p?.type === marker.type && p?.data?.cycleId === markerData.cycleId); if (!alreadyPresent) { msg.content.parts.push(marker); } await this.messageHistory.persistMessages({ messages: [msg], threadId, resourceId }); return; } } } catch (e) { omDebug(`[OM:persistMarkerToStorage] failed to save marker to DB: ${e}`); } } /** * Persist a marker part on the last assistant message in a MessageList * AND save the updated message to the DB. */ async persistMarkerToMessage(marker, messageList, threadId, resourceId) { if (!messageList) return; const allMsgs = messageList.get.all.db(); for (let i = allMsgs.length - 1; i >= 0; i--) { const msg = allMsgs[i]; if (msg?.role === "assistant" && msg.content?.parts && Array.isArray(msg.content.parts)) { const markerData = marker.data; const alreadyPresent = markerData?.cycleId && msg.content.parts.some((p) => p?.type === marker.type && p?.data?.cycleId === markerData.cycleId); if (!alreadyPresent) { msg.content.parts.push(marker); } try { await this.messageHistory.persistMessages({ messages: [msg], threadId, resourceId }); } catch (e) { omDebug(`[OM:persistMarker] failed to save marker to DB: ${e}`); } return; } } } }; var SyncObservationStrategy = class extends ObservationStrategy { startedAt = (/* @__PURE__ */ new Date()).toISOString(); lastMessage; cycleId; tokensToObserve = 0; observerResult; constructor(deps, opts) { super(deps, opts); this.lastMessage = opts.messages[opts.messages.length - 1]; } get needsLock() { return true; } get needsReflection() { return true; } get rethrowOnFailure() { return true; } async prepare() { const { record, threadId, messages } = this.opts; this.deps.emitDebugEvent({ type: "observation_triggered", timestamp: /* @__PURE__ */ new Date(), threadId, resourceId: record.resourceId ?? "", previousObservations: record.activeObservations, messages: messages.map((m) => ({ role: m.role, content: typeof m.content === "string" ? m.content : JSON.stringify(m.content) })) }); const bufferActivation = this.observationConfig.bufferActivation; if (bufferActivation && bufferActivation < 1 && messages.length >= 1) { const newestMsg = messages[messages.length - 1]; if (newestMsg?.content?.parts?.length) { if (!newestMsg.content.metadata) { newestMsg.content.metadata = {}; } const metadata = newestMsg.content.metadata; if (!metadata.mastra) { metadata.mastra = {}; } metadata.mastra.sealed = true; omDebug( `[OM:sync-obs] sealed newest message (${newestMsg.role}, ${newestMsg.content.parts.length} parts) for ratio-aware observation` ); } } this.tokensToObserve = await this.tokenCounter.countMessagesAsync(messages); const freshRecord = await this.storage.getObservationalMemory(record.threadId, record.resourceId); const existingObservations = freshRecord?.activeObservations ?? record.activeObservations ?? ""; return { messages, existingObservations }; } async emitStartMarkers(cycleId) { this.cycleId = cycleId; if (this.lastMessage?.id) { const startMarker = createObservationStartMarker({ cycleId, operationType: "observation", tokensToObserve: this.tokensToObserve, recordId: this.opts.record.id, threadId: this.opts.threadId, threadIds: [this.opts.threadId], config: this.getObservationMarkerConfig() }); await this.streamMarker(startMarker); } } async observe(existingObservations, messages) { const thread = await this.storage.getThreadById({ threadId: this.opts.threadId }); const omMeta = thread ? memory.getThreadOMMetadata(thread.metadata) : void 0; const result = await this.deps.observer.call(existingObservations, messages, this.opts.abortSignal, { requestContext: this.opts.requestContext, observabilityContext: this.opts.observabilityContext, priorCurrentTask: omMeta?.currentTask, priorSuggestedResponse: omMeta?.suggestedResponse, priorThreadTitle: omMeta?.threadTitle }); this.observerResult = result; return result; } async process(output, existingObservations) { const { record, threadId, messages } = this.opts; const lastObservedAt = this.getMaxMessageTimestamp(messages); const messageRange = this.retrieval ? buildMessageRange(messages) : void 0; const newObservations = await this.wrapObservations( output.observations, existingObservations, threadId, lastObservedAt, messageRange ); const observationTokens = this.tokenCounter.countObservations(newObservations); const cycleObservationTokens = this.tokenCounter.countObservations(output.observations); const newMessageIds = messages.map((m) => m.id); const existingIds = record.observedMessageIds ?? []; const observedMessageIds = [.../* @__PURE__ */ new Set([...Array.isArray(existingIds) ? existingIds : [], ...newMessageIds])]; this.deps.emitDebugEvent({ type: "observation_complete", timestamp: /* @__PURE__ */ new Date(), threadId, resourceId: record.resourceId ?? "", observations: newObservations, rawObserverOutput: output.observations, previousObservations: record.activeObservations, messages: messages.map((m) => ({ role: m.role, content: typeof m.content === "string" ? m.content : JSON.stringify(m.content) })), usage: output.usage }); return { observations: newObservations, observationTokens, cycleObservationTokens, observedMessageIds, lastObservedAt, suggestedContinuation: output.suggestedContinuation, currentTask: output.currentTask, threadTitle: output.threadTitle }; } async persist(processed) { const { record, threadId, resourceId, messages } = this.opts; const thread = await this.storage.getThreadById({ threadId }); let threadUpdateMarker; if (thread) { const oldTitle = thread.title?.trim(); const newTitle = processed.threadTitle?.trim(); const shouldUpdateThreadTitle = !!newTitle && newTitle.length >= 3 && newTitle !== oldTitle; const newMetadata = memory.setThreadOMMetadata(thread.metadata, { suggestedResponse: processed.suggestedContinuation, currentTask: processed.currentTask, threadTitle: processed.threadTitle, lastObservedMessageCursor: getLastObservedMessageCursor(messages) }); await this.storage.updateThread({ id: threadId, title: shouldUpdateThreadTitle ? newTitle : thread.title ?? "", metadata: newMetadata }); if (shouldUpdateThreadTitle) { threadUpdateMarker = createThreadUpdateMarker({ cycleId: this.cycleId ?? crypto.randomUUID(), threadId, oldTitle, newTitle }); } } if (threadUpdateMarker) { await this.streamMarker(threadUpdateMarker); } await this.storage.updateActiveObservations({ id: record.id, observations: processed.observations, tokenCount: processed.observationTokens, lastObservedAt: processed.lastObservedAt, observedMessageIds: processed.observedMessageIds }); await this.indexObservationGroups(processed.observations, threadId, resourceId, processed.lastObservedAt); } async emitEndMarkers(cycleId, processed) { const actualTokensObserved = await this.tokenCounter.countMessagesAsync(this.opts.messages); if (this.lastMessage?.id) { const endMarker = createObservationEndMarker({ cycleId, operationType: "observation", startedAt: this.startedAt, tokensObserved: actualTokensObserved, observationTokens: processed.cycleObservationTokens, observations: this.observerResult.observations, currentTask: this.observerResult.currentTask, suggestedResponse: this.observerResult.suggestedContinuation, recordId: this.opts.record.id, threadId: this.opts.threadId }); await this.streamMarker(endMarker); } } async emitFailedMarkers(cycleId, error) { if (this.lastMessage?.id) { const failedMarker = createObservationFailedMarker({ cycleId, operationType: "observation", startedAt: this.startedAt, tokensAttempted: this.tokensToObserve, error: error instanceof Error ? error.message : String(error), recordId: this.opts.record.id, threadId: this.opts.threadId }); await this.streamMarker(failedMarker); } } }; var AsyncBufferObservationStrategy = class extends ObservationStrategy { startedAt; cycleId; constructor(deps, opts) { super(deps, opts); this.cycleId = opts.cycleId; this.startedAt = opts.startedAt ?? (/* @__PURE__ */ new Date()).toISOString(); } get needsLock() { return false; } get needsReflection() { return false; } get rethrowOnFailure() { return false; } generateCycleId() { return this.cycleId; } async prepare() { const { record, messages } = this.opts; const bufferedChunks = getBufferedChunks(record); const bufferedChunksText = bufferedChunks.map((c) => c.observations).join("\n\n"); const existingObservations = combineObservationsForBuffering(record.activeObservations, bufferedChunksText) ?? ""; return { messages, existingObservations }; } async emitStartMarkers(_cycleId) { } async observe(existingObservations, messages) { return this.deps.observer.call(existingObservations, messages, void 0, { skipContinuationHints: true, requestContext: this.opts.requestContext, observabilityContext: this.opts.observabilityContext }); } async process(output, _existingObservations) { const { threadId, messages } = this.opts; if (!output.observations) { omDebug(`[OM:asyncBuffer] empty observations returned, skipping buffer storage`); return { observations: "", observationTokens: 0, cycleObservationTokens: 0, observedMessageIds: [], lastObservedAt: /* @__PURE__ */ new Date() }; } const messageRange = this.retrieval ? buildMessageRange(messages) : void 0; let newObservations; if (this.scope === "resource") { newObservations = await this.wrapWithThreadTag(threadId, output.observations, messageRange); } else { newObservations = this.retrieval && messageRange ? wrapInObservationGroup(output.observations, messageRange) : output.observations; } const observationTokens = this.tokenCounter.countObservations(newObservations); const messageIds = messages.map((m) => m.id); const maxTs = this.getMaxMessageTimestamp(messages); const lastObservedAt = new Date(maxTs.getTime() + 1); return { observations: newObservations, observationTokens, cycleObservationTokens: observationTokens, observedMessageIds: messageIds, lastObservedAt, suggestedContinuation: output.suggestedContinuation, currentTask: output.currentTask, threadTitle: output.threadTitle }; } async persist(processed) { if (!processed.observations) return; const { record, threadId, resourceId, messages } = this.opts; const messageTokens = await this.tokenCounter.countMessagesAsync(messages); await this.storage.updateBufferedObservations({ id: record.id, chunk: { cycleId: this.cycleId, observations: processed.observations, tokenCount: processed.observationTokens, messageIds: processed.observedMessageIds, messageTokens, lastObservedAt: processed.lastObservedAt, suggestedContinuation: processed.suggestedContinuation, currentTask: processed.currentTask, threadTitle: processed.threadTitle }, lastBufferedAtTime: processed.lastObservedAt }); await this.indexObservationGroups(processed.observations, threadId, resourceId, processed.lastObservedAt); const newTitle = processed.threadTitle?.trim(); if (newTitle && newTitle.length >= 3) { const thread = await this.storage.getThreadById({ threadId }); if (thread) { const oldTitle = thread.title?.trim(); if (newTitle !== oldTitle) { const newMetadata = memory.setThreadOMMetadata(thread.metadata, { threadTitle: processed.threadTitle }); await this.storage.updateThread({ id: threadId, title: newTitle, metadata: newMetadata }); const marker = createThreadUpdateMarker({ cycleId: this.cycleId, threadId, oldTitle, newTitle }); await this.streamMarker(marker); } } } } async emitEndMarkers(_cycleId, processed) { if (!processed.observations) return; const { record, threadId, messages } = this.opts; const tokensBuffered = await this.tokenCounter.countMessagesAsync(messages); const updatedRecord = await this.storage.getObservationalMemory(record.threadId, record.resourceId); const updatedChunks = getBufferedChunks(updatedRecord); const totalBufferedTokens = updatedChunks.reduce((sum, c) => sum + (c.tokenCount ?? 0), 0) || processed.observationTokens; const endMarker = createBufferingEndMarker({ cycleId: this.cycleId, operationType: "observation", startedAt: this.startedAt, tokensBuffered, bufferedTokens: totalBufferedTokens, recordId: record.id, threadId, observations: processed.observations }); if (this.opts.writer) { void this.opts.writer.custom({ ...endMarker, transient: true }).catch(() => { }); } await this.persistMarkerToStorage(endMarker, threadId, record.resourceId ?? void 0); } async emitFailedMarkers(_cycleId, error) { const { record, threadId, messages } = this.opts; const tokensAttempted = await this.tokenCounter.countMessagesAsync(messages); const failedMarker = createBufferingFailedMarker({ cycleId: this.cycleId, operationType: "observation", startedAt: this.startedAt, tokensAttempted, error: error instanceof Error ? error.message : String(error), recordId: record.id, threadId }); if (this.opts.writer) { void this.opts.writer.custom({ ...failedMarker, transient: true }).catch(() => { }); } await this.persistMarkerToStorage(failedMarker, threadId, record.resourceId ?? void 0); } }; var ResourceScopedObservationStrategy = class extends ObservationStrategy { startedAt = (/* @__PURE__ */ new Date()).toISOString(); cycleId; resourceId; threadsWithMessages = /* @__PURE__ */ new Map(); threadTokensToObserve = /* @__PURE__ */ new Map(); threadTokenCounts = /* @__PURE__ */ new Map(); threadOrder = []; messagesByThread = /* @__PURE__ */ new Map(); multiThreadResults = /* @__PURE__ */ new Map(); totalBatchUsage = { inputTokens: 0, outputTokens: 0, totalTokens: 0 }; observationResults = []; priorMetadataByThread = /* @__PURE__ */ new Map(); constructor(deps, opts) { super(deps, opts); this.resourceId = opts.resourceId; } get needsLock() { return true; } get needsReflection() { return true; } get rethrowOnFailure() { return true; } async prepare() { const { record, threadId: currentThreadId, messages: currentThreadMessages } = this.opts; const { threads: allThreads } = await this.storage.listThreads({ filter: { resourceId: this.resourceId } }); const threadMetadataMap = /* @__PURE__ */ new Map(); for (const thread of allThreads) { const omMetadata = memory.getThreadOMMetadata(thread.metadata); threadMetadataMap.set(thread.id, { lastObservedAt: omMetadata?.lastObservedAt }); if (omMetadata?.currentTask || omMetadata?.suggestedResponse || omMetadata?.threadTitle) { this.priorMetadataByThread.set(thread.id, { currentTask: omMetadata.currentTask, suggestedResponse: omMetadata.suggestedResponse, threadTitle: omMetadata.threadTitle }); } } for (const thread of allThreads) { const threadLastObservedAt = threadMetadataMap.get(thread.id)?.lastObservedAt; const startDate = threadLastObservedAt ? new Date(new Date(threadLastObservedAt).getTime() + 1) : void 0; const result = await this.storage.listMessages({ threadId: thread.id, perPage: false, orderBy: { field: "createdAt", direction: "ASC" }, filter: startDate ? { dateRange: { start: startDate } } : void 0 }); const messages = result.messages.filter((msg) => msg.role !== "system"); if (messages.length > 0) { this.messagesByThread.set(thread.id, messages); } } if (currentThreadMessages.length > 0) { const existingCurrentThreadMsgs = this.messagesByThread.get(currentThreadId) ?? []; const messageMap = /* @__PURE__ */ new Map(); for (const msg of existingCurrentThreadMsgs) { if (msg.id) messageMap.set(msg.id, msg); } for (const msg of currentThreadMessages) { if (msg.id) messageMap.set(msg.id, msg); } this.messagesByThread.set(currentThreadId, Array.from(messageMap.values())); } for (const [tid, msgs] of this.messagesByThread) { const filtered = msgs.filter((m) => !this.deps.observedMessageIds.has(m.id)); if (filtered.length > 0) { this.messagesByThread.set(tid, filtered); } else { this.messagesByThread.delete(tid); } } let totalMessages = 0; for (const msgs of this.messagesByThread.values()) { totalMessages += msgs.length; } if (totalMessages === 0) { return { messages: [], existingObservations: "" }; } const threshold = getMaxThreshold(this.observationConfig.messageTokens); for (const [threadId, msgs] of this.messagesByThread) { const tokens = await this.tokenCounter.countMessagesAsync(msgs); this.threadTokenCounts.set(threadId, tokens); } const threadsBySize = Array.from(this.messagesByThread.keys()).sort((a, b) => { return (this.threadTokenCounts.get(b) ?? 0) - (this.threadTokenCounts.get(a) ?? 0); }); let accumulatedTokens = 0; const threadsToObserve = []; for (const threadId of threadsBySize) { const threadTokens = this.threadTokenCounts.get(threadId) ?? 0; if (accumulatedTokens >= threshold) break; threadsToObserve.push(threadId); accumulatedTokens += threadTokens; } if (threadsToObserve.length === 0) { return { messages: [], existingObservations: "" }; } this.threadOrder = sortThreadsByOldestMessage( new Map(threadsToObserve.map((tid) => [tid, this.messagesByThread.get(tid) ?? []])) ); for (const threadId of this.threadOrder) { const msgs = this.messagesByThread.get(threadId); if (msgs && msgs.length > 0) { this.threadsWithMessages.set(threadId, msgs); } } this.deps.emitDebugEvent({ type: "observation_triggered", timestamp: /* @__PURE__ */ new Date(), threadId: this.threadOrder.join(","), resourceId: this.resourceId, previousObservations: record.activeObservations, messages: Array.from(this.threadsWithMessages.values()).flat().map((m) => ({ role: m.role, content: typeof m.content === "string" ? m.content : JSON.stringify(m.content) })) }); const freshRecord = await this.storage.getObservationalMemory(null, this.resourceId); const existingObservations = freshRecord?.activeObservations ?? record.activeObservations ?? ""; const allMessages = Array.from(this.threadsWithMessages.values()).flat(); return { messages: allMessages, existingObservations }; } async emitStartMarkers(cycleId) { this.cycleId = cycleId; const allThreadIds = Array.from(this.threadsWithMessages.keys()); for (const [threadId, msgs] of this.threadsWithMessages) { const lastMessage = msgs[msgs.length - 1]; const tokensToObserve = await this.tokenCounter.countMessagesAsync(msgs); this.threadTokensToObserve.set(threadId, tokensToObserve); if (lastMessage?.id) { const startMarker = createObservationStartMarker({ cycleId, operationType: "observation", tokensToObserve, recordId: this.opts.record.id, threadId, threadIds: allThreadIds, config: this.getObservationMarkerConfig() }); await this.streamMarker(startMarker); } } } async observe(_existingObservations, _messages) { const maxTokensPerBatch = this.observationConfig.maxTokensPerBatch ?? chunkD4J4XPGM_cjs.OBSERVATIONAL_MEMORY_DEFAULTS.observation.maxTokensPerBatch; const orderedThreadIds = this.threadOrder.filter((tid) => this.threadsWithMessages.has(tid)); const batches = []; let currentBatch = { threadIds: [], threadMap: /* @__PURE__ */ new Map() }; let currentBatchTokens = 0; for (const threadId of orderedThreadIds) { const msgs = this.threadsWithMessages.get(threadId); const threadTokens = this.threadTokenCounts.get(threadId) ?? 0; if (currentBatchTokens + threadTokens > maxTokensPerBatch && currentBatch.threadIds.length > 0) { batches.push(currentBatch); currentBatch = { threadIds: [], threadMap: /* @__PURE__ */ new Map() }; currentBatchTokens = 0; } currentBatch.threadIds.push(threadId); currentBatch.threadMap.set(threadId, msgs); currentBatchTokens += threadTokens; } if (currentBatch.threadIds.length > 0) { batches.push(currentBatch); } const batchResults = await Promise.all( batches.map(async (batch) => { return this.deps.observer.callMultiThread( _existingObservations, batch.threadMap, batch.threadIds, this.opts.abortSignal, this.opts.requestContext, this.priorMetadataByThread, this.opts.observabilityContext ); }) ); for (const batchResult of batchResults) { for (const [threadId, result] of batchResult.results) { this.multiThreadResults.set(threadId, result); } if (batchResult.usage) { this.totalBatchUsage.inputTokens += batchResult.usage.inputTokens ?? 0; this.totalBatchUsage.outputTokens += batchResult.usage.outputTokens ?? 0; this.totalBatchUsage.totalTokens += batchResult.usage.totalTokens ?? 0; } } return { observations: "", usage: this.totalBatchUsage.totalTokens > 0 ? this.totalBatchUsage : void 0 }; } async process(_output, existingObservations) { const { record } = this.opts; this.observationResults = []; for (const threadId of this.threadOrder) { const threadMessages = this.messagesByThread.get(threadId) ?? []; if (threadMessages.length === 0) continue; const result = this.multiThreadResults.get(threadId); if (!result) continue; this.observationResults.push({ threadId, threadMessages, result }); } let currentObservations = existingObservations; let cycleObservationTokens = 0; const threadMetadataUpdates = []; for (const obsResult of this.observationResults) { const { threadId, threadMessages, result } = obsResult; cycleObservationTokens += this.tokenCounter.countObservations(result.observations); const messageRange = this.retrieval ? buildMessageRange(threadMessages) : void 0; const threadSection = await this.wrapWithThreadTag(threadId, result.observations, messageRange); const threadLastObservedAt = this.getMaxMessageTimestamp(threadMessages); currentObservations = this.replaceOrAppendThreadSection( currentObservations, threadId, threadSection, threadLastObservedAt ); threadMetadataUpdates.push({ threadId, lastObservedAt: threadLastObservedAt.toISOString(), suggestedResponse: result.suggestedContinuation, currentTask: result.currentTask, threadTitle: result.threadTitle, lastObservedMessageCursor: getLastObservedMessageCursor(threadMessages) }); const isFirstThread = this.observationResults.indexOf(obsResult) === 0; this.deps.emitDebugEvent({ type: "observation_complete", timestamp: /* @__PURE__ */ new Date(), threadId, resourceId: this.resourceId, observations: threadSection, rawObserverOutput: result.observations, previousObservations: record.activeObservations, messages: threadMessages.map((m) => ({ role: m.role, content: typeof m.content === "string" ? m.content : JSON.stringify(m.content) })), usage: isFirstThread && this.totalBatchUsage.totalTokens > 0 ? this.totalBatchUsage : void 0 }); } const observedMessages = this.observationResults.flatMap((r) => r.threadMessages); const lastObservedAt = this.getMaxMessageTimestamp(observedMessages); const newMessageIds = observedMessages.map((m) => m.id); const existingIds = record.observedMessageIds ?? []; const observedMessageIds = [.../* @__PURE__ */ new Set([...existingIds, ...newMessageIds])]; const observationTokens = this.tokenCounter.countObservations(currentObservations); return { observations: currentObservations, observationTokens, cycleObservationTokens, observedMessageIds, lastObservedAt, threadMetadataUpdates }; } async persist(processed) { const { record, resourceId } = this.opts; const threadUpdateMarkers = []; if (processed.threadMetadataUpdates) { for (const update of processed.threadMetadataUpdates) { const thread = await this.storage.getThreadById({ threadId: update.threadId }); if (thread) { const oldTitle = thread.title?.trim(); const newTitle = update.threadTitle?.trim(); const shouldUpdateThreadTitle = !!newTitle && newTitle.length >= 3 && newTitle !== oldTitle; const newMetadata = memory.setThreadOMMetadata(thread.metadata, { lastObservedAt: update.lastObservedAt, suggestedResponse: update.suggestedResponse, currentTask: update.currentTask, threadTitle: update.threadTitle, lastObservedMessageCursor: update.lastObservedMessageCursor }); await this.storage.updateThread({ id: update.threadId, title: shouldUpdateThreadTitle ? newTitle : thread.title ?? "", metadata: newMetadata }); if (shouldUpdateThreadTitle) { threadUpdateMarkers.push( createThreadUpdateMarker({ cycleId: this.cycleId ?? crypto.randomUUID(), threadId: update.threadId, oldTitle, newTitle }) ); } } } } for (const marker of threadUpdateMarkers) { await this.streamMarker(marker); } await this.storage.updateActiveObservations({ id: record.id, observations: processed.observations, tokenCount: processed.observationTokens, lastObservedAt: processed.lastObservedAt, observedMessageIds: processed.observedMessageIds }); if (resourceId) { await Promise.all( this.observationResults.map( ({ threadId, threadMessages, result }) => this.indexObservationGroups( result.observations, threadId, resourceId, this.getMaxMessageTimestamp(threadMessages) ) ) ); } } async emitEndMarkers(cycleId, processed) { for (const obsResult of this.observationResults) { const { threadId, threadMessages, result } = obsResult; const lastMessage = threadMessages[threadMessages.length - 1]; if (lastMessage?.id) { const tokensObserved = this.threadTokensToObserve.get(threadId) ?? await this.tokenCounter.countMessagesAsync(threadMessages); const endMarker = createObservationEndMarker({ cycleId, operationType: "observation", startedAt: this.startedAt, tokensObserved, observationTokens: processed.cycleObservationTokens, observations: result.observations, currentTask: result.currentTask, suggestedResponse: result.suggestedContinuation, recordId: this.opts.record.id, threadId }); await this.streamMarker(endMarker); } } } async emitFailedMarkers(cycleId, error) { for (const [threadId, msgs] of this.threadsWithMessages) { const lastMessage = msgs[msgs.length - 1]; if (lastMessage?.id) { const tokensAttempted = this.threadTokensToObserve.get(threadId) ?? 0; const failedMarker = createObservationFailedMarker({ cycleId, operationType: "observation", startedAt: this.startedAt, tokensAttempted, error: error instanceof Error ? error.message : String(error), recordId: this.opts.record.id, threadId }); await this.streamMarker(failedMarker); } } } }; // src/processors/observational-memory/observation-strategies/index.ts ObservationStrategy.create = ((om, opts) => { const deps = { storage: om.getStorage(), messageHistory: om.getMessageHistory(), tokenCounter: om.getTokenCounter(), observationConfig: om.getObservationConfig(), reflectionConfig: om.getReflectionConfig(), scope: om.scope, retrieval: om.retrieval, observer: om.observer, reflector: om.reflector, observedMessageIds: om.observedMessageIds, obscureThreadIds: om.getObscureThreadIds(), onIndexObservations: om.onIndexObservations, emitDebugEvent: (e) => om.emitDebugEvent(e) }; if (opts.cycleId) return new AsyncBufferObservationStrategy(deps, opts); if (deps.scope === "resource" && opts.resourceId) return new ResourceScopedObservationStrategy(deps, opts); return new SyncObservationStrategy(deps, opts); }); // src/processors/observational-memory/observation-turn/load-memory-context.ts async function loadMemoryContextMessages({ memory, messageList, threadId, resourceId }) { const ctx = await memory.getContext({ threadId, resourceId }); for (const msg of ctx.messages) { if (msg.role !== "system") { messageList.add(msg, "memory"); } } return ctx; } var ObservationStep = class { constructor(turn, stepNumber) { this.turn = turn; this.stepNumber = stepNumber; } turn; stepNumber; _prepared = false; _context; /** Whether this step has been prepared. */ get prepared() { return this._prepared; } /** Step context from prepare(). Throws if prepare() hasn't been called. */ get context() { if (!this._context) throw new Error("Step not prepared yet \u2014 call prepare() first"); return this._context; } /** * Prepare this step for agent generation. * * For step 0: activates buffered chunks, checks reflection, builds system message, filters observed. * For step > 0: checks thresholds, triggers buffer/observe, saves previous messages, * builds system message, filters observed. */ async prepare() { if (this._prepared) throw new Error(`Step ${this.stepNumber} already prepared`); const { threadId, resourceId, messageList } = this.turn; const om = this.turn.om; let activated = false; let observed = false; let buffered = false; let reflected = false; let didThresholdCleanup = false; let observerExchange; if (this.stepNumber === 0) { const step0Messages = messageList.get.all.db(); const activation = await om.activate({ threadId, resourceId, checkThreshold: true, messages: step0Messages, currentModel: this.turn.actorModelContext, writer: this.turn.writer, messageList }); if (activation.activated) { activated = true; if (activation.activatedMessageIds?.length) { messageList.removeByIds(activation.activatedMessageIds); } await om.resetBufferingState({ threadId, resourceId, recordId: activation.record.id }); await this.turn.refreshRecord(); } const record = this.turn.record; const preReflectGeneration = record.generationCount; const obsTokens = record.observationTokenCount ?? 0; await om.reflector.maybeReflect({ record, observationTokens: obsTokens, threadId, writer: this.turn.writer, messageList, currentModel: this.turn.actorModelContext, requestContext: this.turn.requestContext, observabilityContext: this.turn.observabilityContext, lastActivityAt: getLastActivityFromMessages(messageList.get.all.db()) }); await this.turn.refreshRecord(); if (this.turn.record.generationCount > preReflectGeneration) { reflected = true; } } const allMsgsForToolCheck = messageList.get.all.db(); const lastMessage = allMsgsForToolCheck[allMsgsForToolCheck.length - 1]; const pendingStepMessages = [...messageList.get.input.db(), ...messageList.get.response.db()]; const latestStepParts = [ ...getLatestStepParts(lastMessage?.content?.parts ?? []), ...pendingStepMessages.flatMap((msg) => getLatestStepParts(msg.content?.parts ?? [])) ]; const hasIncompleteToolCalls = latestStepParts.some( (part) => part?.type === "tool-invocation" && part.toolInvocation?.state === "call" ); omDebug( `[OM:deferred-check] hasIncompleteToolCalls=${hasIncompleteToolCalls}, latestStepPartsCount=${latestStepParts.length}` ); let statusSnapshot = await om.getStatus({ threadId, resourceId, messages: messageList.get.all.db() }); if (statusSnapshot.shouldBuffer && !hasIncompleteToolCalls) { const allMessages = messageList.get.all.db(); const unobservedMessages = om.getUnobservedMessages(allMessages, statusSnapshot.record); const candidates = om.getUnobservedMessages(unobservedMessages, statusSnapshot.record, { excludeBuffered: true }); if (candidates.length > 0) { om.sealMessagesForBuffering(candidates); try { await this.turn.hooks?.onBufferChunkSealed?.(); } catch (error) { omDebug( `[OM:buffer] onBufferChunkSealed hook failed: ${error instanceof Error ? error.message : String(error)}` ); } if (this.turn.memory) { await this.turn.memory.persistMessages(candidates); } messageList.removeByIds(candidates.map((msg) => msg.id)); for (const msg of candidates) { messageList.add(msg, "memory"); } } void om.buffer({ threadId, resourceId, messages: unobservedMessages, pendingTokens: statusSnapshot.pendingTokens, record: statusSnapshot.record, writer: this.turn.writer, requestContext: this.turn.requestContext, observabilityContext: this.turn.observabilityContext }).catch((err) => { omDebug(`[OM:buffer] fire-and-forget buffer failed: ${err?.message}`); }); buffered = true; } if (this.stepNumber > 0) { const newInput = messageList.clear.input.db(); const newOutput = messageList.clear.response.db(); const messagesToSave = [...newInput, ...newOutput]; if (messagesToSave.length > 0) { await om.persistMessages(messagesToSave, threadId, resourceId); for (const msg of messagesToSave) { messageList.add(msg, "memory"); } } if (statusSnapshot.shouldObserve && !hasIncompleteToolCalls) { const preObsGeneration = this.turn.record.generationCount; const obsResult = await this.runThresholdObservation(); observerExchange = obsResult.observerExchange; if (obsResult.succeeded) { observed = true; didThresholdCleanup = true; const observedIds = obsResult.activatedMessageIds ?? obsResult.record.observedMessageIds ?? []; const minRemaining = resolveRetentionFloor( om.getObservationConfig().bufferActivation ?? 1, statusSnapshot.threshold ); await om.cleanupMessages({ threadId, resourceId, messages: messageList, observedMessageIds: observedIds, retentionFloor: minRemaining }); if (statusSnapshot.asyncObservationEnabled) { await om.resetBufferingState({ threadId, resourceId, recordId: obsResult.record.id, activatedMessageIds: obsResult.activatedMessageIds }); } await this.turn.refreshRecord(); if (this.turn.record.generationCount > preObsGeneration) { reflected = true; } } } statusSnapshot = await om.getStatus({ threadId, resourceId, messages: messageList.get.all.db() }); } const otherThreadsContext = await this.turn.refreshOtherThreadsContext(); const systemMessage = await om.buildContextSystemMessages({ threadId, resourceId, record: this.turn.record, unobservedContextBlocks: otherThreadsContext }); if (!didThresholdCleanup) { const fallbackCursor = this.turn.record.threadId ? memory.getThreadOMMetadata((await om.getStorage().getThreadById({ threadId: this.turn.record.threadId }))?.metadata)?.lastObservedMessageCursor : void 0; const pendingMessageIds = new Set( [...messageList.get.input.db(), ...messageList.get.response.db()].map((msg) => msg.id).filter(Boolean) ); filterObservedMessages({ messageList, record: this.turn.record, useMarkerBoundaryPruning: this.stepNumber === 0, fallbackCursor, preserveMessageIds: pendingMessageIds }); } this._context = { systemMessage, observerExchange, activated, observed, buffered, reflected, status: { pendingTokens: statusSnapshot.pendingTokens, threshold: statusSnapshot.threshold, effectiveObservationTokensThreshold: statusSnapshot.effectiveObservationTokensThreshold, shouldObserve: statusSnapshot.shouldObserve, shouldBuffer: statusSnapshot.shouldBuffer, shouldReflect: statusSnapshot.shouldReflect, canActivate: statusSnapshot.canActivate } }; this._prepared = true; return this._context; } /** * Run the full threshold observation pipeline: * waitForBuffering → re-check → activate → reflect → blockAfter gate → observe */ async runThresholdObservation() { const { threadId, resourceId, messageList } = this.turn; const om = this.turn.om; await om.waitForBuffering(threadId, resourceId); const freshStatus = await om.getStatus({ threadId, resourceId, messages: messageList.get.all.db() }); if (!freshStatus.shouldObserve) { return { succeeded: false, record: freshStatus.record }; } if (freshStatus.canActivate) { const activation = await om.activate({ threadId, resourceId, messages: messageList.get.all.db(), currentModel: this.turn.actorModelContext, writer: this.turn.writer, messageList }); if (activation.activated) { const postActivationRecord = activation.record; await om.reflector.maybeReflect({ record: postActivationRecord, observationTokens: postActivationRecord.observationTokenCount ?? 0, threadId, writer: this.turn.writer, messageList, currentModel: this.turn.actorModelContext, requestContext: this.turn.requestContext, observabilityContext: this.turn.observabilityContext, lastActivityAt: getLastActivityFromMessages(messageList.get.all.db()) }); return { succeeded: true, record: activation.record, activatedMessageIds: activation.activatedMessageIds }; } } const obsResult = await om.observe({ threadId, resourceId, messages: messageList.get.all.db(), requestContext: this.turn.requestContext, writer: this.turn.writer, observabilityContext: this.turn.observabilityContext }); if (obsResult.observed) { const observedMessageIds = new Set(obsResult.record.observedMessageIds ?? []); const liveMessages = messageList.get.all.db(); let latestObservedIndex = -1; for (let i = liveMessages.length - 1; i >= 0; i--) { const message = liveMessages[i]; if (message && observedMessageIds.has(message.id)) { latestObservedIndex = i; break; } } const messageToSeal = latestObservedIndex >= 0 ? liveMessages[latestObservedIndex] : void 0; const messagesToSeal = messageToSeal ? [messageToSeal] : []; om.sealMessagesForBuffering(messagesToSeal); try { await this.turn.hooks?.onSyncObservationComplete?.(); } catch (error) { omDebug( `[OM:observe] onSyncObservationComplete hook failed: ${error instanceof Error ? error.message : String(error)}` ); } if (messagesToSeal.length > 0) { await om.persistMessages(messagesToSeal, threadId, resourceId); } } return { succeeded: obsResult.observed, record: obsResult.record, observerExchange: om.observer.lastExchange }; } }; // src/processors/observational-memory/observation-turn/turn.ts var ObservationTurn = class { _record; _context; _currentStep; _started = false; _ended = false; /** Generation count at turn start — used to detect if reflection happened during the turn. */ _generationCountAtStart = -1; /** Memory context provider — set via start(). Used by steps for beforeBuffer persistence. */ memory; /** Optional stream writer for emitting markers. */ writer; /** Optional request context for observation calls. */ requestContext; /** Optional observability context for nested OM spans. */ observabilityContext; /** Optional signal sender for processor-originated notifications. */ sendSignal; /** Current actor model for this step. Updated by the processor before prepare(). */ actorModelContext; /** Processor-provided hooks for turn/step lifecycle integration. */ hooks; constructor(opts) { this.om = opts.om; this.threadId = opts.threadId; this.resourceId = opts.resourceId; this.messageList = opts.messageList; this.sendSignal = opts.sendSignal; this.requestContext = opts.requestContext; this.observabilityContext = opts.observabilityContext; this.hooks = opts.hooks ?? {}; } om; threadId; resourceId; messageList; /** The current cached record. Refreshed after mutations (activate/observe/reflect). */ get record() { if (!this._record) throw new Error("Turn not started \u2014 call start() first"); return this._record; } /** The context loaded during start(). */ get context() { if (!this._context) throw new Error("Turn not started \u2014 call start() first"); return this._context; } /** The current step, if one exists. */ get currentStep() { return this._currentStep; } addHooks(hooks) { if (!hooks) return; Object.assign(this.hooks, hooks); } /** * Load context and cache the record. Call once at the start of the turn. * * If a MemoryContextProvider is passed, loads historical messages and adds * them to the MessageList. Without a provider, only fetches/caches the record. */ async start(memory) { if (this._started) throw new Error("Turn already started"); this._started = true; this._record = await this.om.getOrCreateRecord(this.threadId, this.resourceId); this._generationCountAtStart = this._record.generationCount; this.memory = memory; if (memory) { const ctx = await loadMemoryContextMessages({ memory, messageList: this.messageList, threadId: this.threadId, resourceId: this.resourceId }); this._context = { messages: ctx.messages, systemMessage: ctx.systemMessage, continuation: ctx.continuationMessage, otherThreadsContext: ctx.otherThreadsContext, record: this._record }; } else { this._context = { messages: [], systemMessage: void 0, continuation: void 0, otherThreadsContext: void 0, record: this._record }; } return this._context; } /** * Create a step handle. If a previous step exists, it is finalized * (its output messages will be saved at the start of the new step's prepare()). */ step(stepNumber) { if (!this._started) throw new Error("Turn not started \u2014 call start() first"); if (this._ended) throw new Error("Turn already ended"); this._currentStep = new ObservationStep(this, stepNumber); return this._currentStep; } /** * Finalize the turn: save any remaining messages and return the current cached record. * * When async observation buffering is enabled and there are unobserved messages, * a background buffer operation is kicked off so that observations are computed * proactively while the agent is idle, rather than waiting for the next turn. * The returned record does not wait for that background buffering pass to finish. */ async end() { if (this._ended) throw new Error("Turn already ended"); this._ended = true; const unsavedInput = this.messageList.get.input.db(); const unsavedOutput = this.messageList.get.response.db(); const unsavedMessages = [...unsavedInput, ...unsavedOutput]; if (unsavedMessages.length > 0) { await this.om.persistMessages(unsavedMessages, this.threadId, this.resourceId); } const asyncObservationEnabled = this.om.buffering.isAsyncObservationEnabled(); const bufferOnIdle = this.om.getObservationConfig().bufferOnIdle; if (asyncObservationEnabled && bufferOnIdle) { const allMessages = this.messageList.get.all.db(); const record = this._record; const unobservedMessages = this.om.getUnobservedMessages(allMessages, record); if (unobservedMessages.length > 0) { void this.om.buffer({ threadId: this.threadId, resourceId: this.resourceId, messages: unobservedMessages, record, writer: this.writer, sendSignal: this.sendSignal, requestContext: this.requestContext, currentModel: this.actorModelContext, observabilityContext: this.observabilityContext, skipMinimumTokenCheck: true }).catch((err) => { omDebug(`[OM:turn.end] idle buffer failed: ${err?.message}`); }); } } return { record: this._record }; } /** * Refresh the cached record from storage. Called internally after mutations. * @internal */ async refreshRecord() { this._record = await this.om.getOrCreateRecord(this.threadId, this.resourceId); } /** * Refresh cross-thread context for resource scope. Called per-step. * @internal */ async refreshOtherThreadsContext() { if (this.om.scope === "resource" && this.resourceId) { const otherThreadsContext = await this.om.getOtherThreadsContext(this.resourceId, this.threadId); if (this._context) { this._context.otherThreadsContext = otherThreadsContext; } return otherThreadsContext; } return this._context?.otherThreadsContext; } }; // src/processors/observational-memory/anchor-ids.ts var ANCHOR_ID_PATTERN = /^\[(O\d+(?:-N\d+)?)\]\s*/; var OBSERVATION_DATE_HEADER_PATTERN = /^\s*Date:\s+/; var XML_TAG_PATTERN = /^\s*<\/?[a-z][^>]*>\s*$/i; var MARKDOWN_GROUP_HEADING_PATTERN = /^\s*##\s+Group\s+`[^`]+`\s*$/; var MARKDOWN_GROUP_METADATA_PATTERN = /^\s*_range:\s*`[^`]*`_\s*$/; function buildEphemeralAnchorId(topLevelCounter, nestedCounter) { return nestedCounter === 0 ? `O${topLevelCounter}` : `O${topLevelCounter}-N${nestedCounter}`; } function parseAnchorId(line) { const match = line.match(ANCHOR_ID_PATTERN); return match?.[1] ?? null; } function shouldAnchorLine(line) { const trimmed = line.trim(); if (!trimmed) { return false; } if (parseAnchorId(trimmed)) { return false; } if (OBSERVATION_DATE_HEADER_PATTERN.test(trimmed)) { return false; } if (XML_TAG_PATTERN.test(trimmed)) { return false; } if (MARKDOWN_GROUP_HEADING_PATTERN.test(trimmed) || MARKDOWN_GROUP_METADATA_PATTERN.test(trimmed)) { return false; } return true; } function getIndentationDepth(line) { const leadingWhitespace = line.match(/^\s*/)?.[0] ?? ""; return Math.floor(leadingWhitespace.replace(/\t/g, " ").length / 2); } function injectAnchorIds(observations) { if (!observations) { return observations; } const lines = observations.split("\n"); let topLevelCounter = 0; let nestedCounter = 0; let changed = false; for (let i = 0; i < lines.length; i++) { const line = lines[i]; if (!shouldAnchorLine(line)) { continue; } const indentationDepth = getIndentationDepth(line); if (indentationDepth === 0) { topLevelCounter += 1; nestedCounter = 0; } else { if (topLevelCounter === 0) { topLevelCounter = 1; } nestedCounter += 1; } const anchorId = buildEphemeralAnchorId(topLevelCounter, nestedCounter); const leadingWhitespace = line.match(/^\s*/)?.[0] ?? ""; lines[i] = `${leadingWhitespace}[${anchorId}] ${line.slice(leadingWhitespace.length)}`; changed = true; } return changed ? lines.join("\n") : observations; } function stripEphemeralAnchorIds(observations) { if (!observations) { return observations; } return observations.replace(/(^|\n)([^\S\n]*)\[(O\d+(?:-N\d+)?)\][^\S\n]*/g, "$1$2"); } // src/processors/observational-memory/string-utils.ts function safeSlice(str, end) { if (end <= 0) return ""; if (end >= str.length) return str; const code = str.charCodeAt(end - 1); const safeEnd = code >= 55296 && code <= 56319 ? end - 1 : end; return str.slice(0, safeEnd); } var ENCRYPTED_CONTENT_KEY = "encryptedContent"; var ENCRYPTED_CONTENT_REDACTION_THRESHOLD = 256; var DEFAULT_OBSERVER_TOOL_RESULT_MAX_TOKENS = 1e4; function isObjectLike(value) { return typeof value === "object" && value !== null; } function sanitizeToolResultValue(value, seen = /* @__PURE__ */ new WeakMap()) { if (!isObjectLike(value)) { return value; } if (seen.has(value)) { return seen.get(value); } if (Array.isArray(value)) { const sanitizedArray = []; seen.set(value, sanitizedArray); for (const item of value) { sanitizedArray.push(sanitizeToolResultValue(item, seen)); } return sanitizedArray; } const sanitizedObject = {}; seen.set(value, sanitizedObject); for (const [key, entry] of Object.entries(value)) { if (key === ENCRYPTED_CONTENT_KEY && typeof entry === "string" && entry.length > ENCRYPTED_CONTENT_REDACTION_THRESHOLD) { sanitizedObject[key] = `[stripped encryptedContent: ${entry.length} characters]`; continue; } sanitizedObject[key] = sanitizeToolResultValue(entry, seen); } return sanitizedObject; } function stringifyToolResult(value) { if (typeof value === "string") { return value; } const sanitized = sanitizeToolResultValue(value); try { return JSON.stringify(sanitized, null, 2); } catch { return String(sanitized); } } function resolveToolResultValue(part, invocationResult) { const mastraMetadata = part?.providerMetadata?.mastra; if (mastraMetadata && typeof mastraMetadata === "object" && "modelOutput" in mastraMetadata) { return { value: mastraMetadata.modelOutput, usingStoredModelOutput: true }; } return { value: invocationResult, usingStoredModelOutput: false }; } function truncateStringByTokens(text, maxTokens) { if (!text || maxTokens <= 0) { return ""; } const totalTokens = tokenx.estimateTokenCount(text); if (totalTokens <= maxTokens) { return text; } const buildCandidate = (sliceEnd) => { const visible = safeSlice(text, sliceEnd); return `${visible} ... [truncated ~${totalTokens - tokenx.estimateTokenCount(visible)} tokens]`; }; let low = 0; let high = text.length; let best = buildCandidate(0); while (low <= high) { const mid = Math.floor((low + high) / 2); const candidate = buildCandidate(mid); const candidateTokens = tokenx.estimateTokenCount(candidate); if (candidateTokens <= maxTokens) { best = candidate; low = mid + 1; } else { high = mid - 1; } } return best; } function formatToolResultForObserver(value, options) { const serialized = stringifyToolResult(value); const maxTokens = options?.maxTokens ?? DEFAULT_OBSERVER_TOOL_RESULT_MAX_TOKENS; return truncateStringByTokens(serialized, maxTokens); } // src/processors/observational-memory/observer-agent.ts var OBSERVER_EXTRACTION_INSTRUCTIONS = `CRITICAL: DISTINGUISH USER ASSERTIONS FROM QUESTIONS When the user TELLS you something about themselves, mark it as an assertion: - "I have two kids" \u2192 \u{1F534} (14:30) User stated has two kids - "I work at Acme Corp" \u2192 \u{1F534} (14:31) User stated works at Acme Corp - "I graduated in 2019" \u2192 \u{1F534} (14:32) User stated graduated in 2019 When the user ASKS about something, mark it as a question/request: - "Can you help me with X?" \u2192 \u{1F534} (15:00) User asked help with X - "What's the best way to do Y?" \u2192 \u{1F534} (15:01) User asked best way to do Y Distinguish between QUESTIONS and STATEMENTS OF INTENT: - "Can you recommend..." \u2192 Question (extract as "User asked...") - "I'm looking forward to [doing X]" \u2192 Statement of intent (extract as "User stated they will [do X] (include estimated/actual date if mentioned)") - "I need to [do X]" \u2192 Statement of intent (extract as "User stated they need to [do X] (again, add date if mentioned)") STATE CHANGES AND UPDATES: When a user indicates they are changing something, frame it as a state change that supersedes previous information: - "I'm going to start doing X instead of Y" \u2192 "User will start doing X (changing from Y)" - "I'm switching from A to B" \u2192 "User is switching from A to B" - "I moved my stuff to the new place" \u2192 "User moved their stuff to the new place (no longer at previous location)" If the new state contradicts or updates previous information, make that explicit: - BAD: "User plans to use the new method" - GOOD: "User will use the new method (replacing the old approach)" This helps distinguish current state from outdated information. USER ASSERTIONS ARE AUTHORITATIVE. The user is the source of truth about their own life. If a user previously stated something and later asks a question about the same topic, the assertion is the answer - the question doesn't invalidate what they already told you. TEMPORAL ANCHORING: Each observation has TWO potential timestamps: 1. BEGINNING: The time the statement was made (from the message timestamp) - ALWAYS include this 2. END: The time being REFERENCED, if different from when it was said - ONLY when there's a relative time reference ONLY add "(meaning DATE)" or "(estimated DATE)" at the END when you can provide an ACTUAL DATE: - Past: "last week", "yesterday", "a few days ago", "last month", "in March" - Future: "this weekend", "tomorrow", "next week" DO NOT add end dates for: - Present-moment statements with no time reference - Vague references like "recently", "a while ago", "lately", "soon" - these cannot be converted to actual dates FORMAT: - With time reference: (TIME) [observation]. (meaning/estimated DATE) - Without time reference: (TIME) [observation]. GOOD: (09:15) User's friend had a birthday party in March. (meaning March 20XX) ^ References a past event - add the referenced date at the end GOOD: (09:15) User will visit their parents this weekend. (meaning June 17-18, 20XX) ^ References a future event - add the referenced date at the end GOOD: (09:15) User prefers hiking in the mountains. ^ Present-moment preference, no time reference - NO end date needed GOOD: (09:15) User is considering adopting a dog. ^ Present-moment thought, no time reference - NO end date needed BAD: (09:15) User prefers hiking in the mountains. (meaning June 15, 20XX - today) ^ No time reference in the statement - don't repeat the message timestamp at the end IMPORTANT: If an observation contains MULTIPLE events, split them into SEPARATE observation lines. EACH split observation MUST have its own date at the end - even if they share the same time context. Examples (assume message is from June 15, 20XX): BAD: User will visit their parents this weekend (meaning June 17-18, 20XX) and go to the dentist tomorrow. GOOD (split into two observations, each with its date): User will visit their parents this weekend. (meaning June 17-18, 20XX) User will go to the dentist tomorrow. (meaning June 16, 20XX) BAD: User needs to clean the garage this weekend and is looking forward to setting up a new workbench. GOOD (split, BOTH get the same date since they're related): User needs to clean the garage this weekend. (meaning June 17-18, 20XX) User will set up a new workbench this weekend. (meaning June 17-18, 20XX) BAD: User was given a gift by their friend (estimated late May 20XX) last month. GOOD: (09:15) User was given a gift by their friend last month. (estimated late May 20XX) ^ Message time at START, relative date reference at END - never in the middle BAD: User started a new job recently and will move to a new apartment next week. GOOD (split): User started a new job recently. User will move to a new apartment next week. (meaning June 21-27, 20XX) ^ "recently" is too vague for a date - omit the end date. "next week" can be calculated. ALWAYS put the date at the END in parentheses - this is critical for temporal reasoning. When splitting related events that share the same time context, EACH observation must have the date. PRESERVE UNUSUAL PHRASING: When the user uses unexpected or non-standard terminology, quote their exact words. BAD: User exercised. GOOD: User stated they did a "movement session" (their term for exercise). USE PRECISE ACTION VERBS: Replace vague verbs like "getting", "got", "have" with specific action verbs that clarify the nature of the action. If the assistant confirms or clarifies the user's action, use the assistant's more precise language. BAD: User is getting X. GOOD: User subscribed to X. (if context confirms recurring delivery) GOOD: User purchased X. (if context confirms one-time acquisition) BAD: User got something. GOOD: User purchased / received / was given something. (be specific) Common clarifications: - "getting" something regularly \u2192 "subscribed to" or "enrolled in" - "getting" something once \u2192 "purchased" or "acquired" - "got" \u2192 "purchased", "received as gift", "was given", "picked up" - "signed up" \u2192 "enrolled in", "registered for", "subscribed to" - "stopped getting" \u2192 "canceled", "unsubscribed from", "discontinued" When the assistant interprets or confirms the user's vague language, prefer the assistant's precise terminology. PRESERVING DETAILS IN ASSISTANT-GENERATED CONTENT: When the assistant provides lists, recommendations, or creative content that the user explicitly requested, preserve the DISTINGUISHING DETAILS that make each item unique and queryable later. 1. RECOMMENDATION LISTS - Preserve the key attribute that distinguishes each item: BAD: Assistant recommended 5 hotels in the city. GOOD: Assistant recommended hotels: Hotel A (near the train station), Hotel B (budget-friendly), Hotel C (has rooftop pool), Hotel D (pet-friendly), Hotel E (historic building). BAD: Assistant listed 3 online stores for craft supplies. GOOD: Assistant listed craft stores: Store A (based in Germany, ships worldwide), Store B (specializes in vintage fabrics), Store C (offers bulk discounts). 2. NAMES, HANDLES, AND IDENTIFIERS - Always preserve specific identifiers: BAD: Assistant provided social media accounts for several photographers. GOOD: Assistant provided photographer accounts: @photographer_one (portraits), @photographer_two (landscapes), @photographer_three (nature). BAD: Assistant listed some authors to check out. GOOD: Assistant recommended authors: Jane Smith (mystery novels), Bob Johnson (science fiction), Maria Garcia (historical romance). 3. CREATIVE CONTENT - Preserve structure and key sequences: BAD: Assistant wrote a poem with multiple verses. GOOD: Assistant wrote a 3-verse poem. Verse 1 theme: loss. Verse 2 theme: hope. Verse 3 theme: renewal. Refrain: "The light returns." BAD: User shared their lucky numbers from a fortune cookie. GOOD: User's fortune cookie lucky numbers: 7, 14, 23, 38, 42, 49. 4. TECHNICAL/NUMERICAL RESULTS - Preserve specific values: BAD: Assistant explained the performance improvements from the optimization. GOOD: Assistant explained the optimization achieved 43.7% faster load times and reduced memory usage from 2.8GB to 940MB. BAD: Assistant provided statistics about the dataset. GOOD: Assistant provided dataset stats: 7,342 samples, 89.6% accuracy, 23ms average inference time. 5. QUANTITIES AND COUNTS - Always preserve how many of each item: BAD: Assistant listed items with details but no quantities. GOOD: Assistant listed items: Item A (4 units, size large), Item B (2 units, size small). When listing items with attributes, always include the COUNT first before other details. 6. ROLE/PARTICIPATION STATEMENTS - When user mentions their role at an event: BAD: User attended the company event. GOOD: User was a presenter at the company event. BAD: User went to the fundraiser. GOOD: User volunteered at the fundraiser (helped with registration). Always capture specific roles: presenter, organizer, volunteer, team lead, coordinator, participant, contributor, helper, etc. CONVERSATION CONTEXT: - What the user is working on or asking about - Previous topics and their outcomes - What user understands or needs clarification on - Specific requirements or constraints mentioned - Contents of assistant learnings and summaries - Answers to users questions including full context to remember detailed summaries and explanations - Assistant explanations, especially complex ones. observe the fine details so that the assistant does not forget what they explained - Relevant code snippets - User preferences (like favourites, dislikes, preferences, etc) - Any specifically formatted text or ascii that would need to be reproduced or referenced in later interactions (preserve these verbatim in memory) - Sequences, units, measurements, and any kind of specific relevant data - Any blocks of any text which the user and assistant are iteratively collaborating back and forth on should be preserved verbatim - When who/what/where/when is mentioned, note that in the observation. Example: if the user received went on a trip with someone, observe who that someone was, where the trip was, when it happened, and what happened, not just that the user went on the trip. - For any described entity (like a person, place, thing, etc), preserve the attributes that would help identify or describe the specific entity later: location ("near X"), specialty ("focuses on Y"), unique feature ("has Z"), relationship ("owned by W"), or other details. The entity's name is important, but so are any additional details that distinguish it. If there are a list of entities, preserve these details for each of them. USER MESSAGE CAPTURE: - Short and medium-length user messages should be captured nearly verbatim in your own words. - For very long user messages, summarize but quote key phrases that carry specific intent or meaning. - This is critical for continuity: when the conversation window shrinks, the observations are the only record of what the user said. AVOIDING REPETITIVE OBSERVATIONS: - Do NOT repeat the same observation across multiple turns if there is no new information. - When the agent performs repeated similar actions (e.g., browsing files, running the same tool type multiple times), group them into a single parent observation with sub-bullets for each new result. Example \u2014 BAD (repetitive): * \u{1F7E1} (14:30) Agent used view tool on src/auth.ts * \u{1F7E1} (14:31) Agent used view tool on src/users.ts * \u{1F7E1} (14:32) Agent used view tool on src/routes.ts Example \u2014 GOOD (grouped): * \u{1F7E1} (14:30) Agent browsed source files for auth flow * -> viewed src/auth.ts \u2014 found token validation logic * -> viewed src/users.ts \u2014 found user lookup by email * -> viewed src/routes.ts \u2014 found middleware chain Only add a new observation for a repeated action if the NEW result changes the picture. ACTIONABLE INSIGHTS: - What worked well in explanations - What needs follow-up or clarification - User's stated goals or next steps (note if the user tells you not to do a next step, or asks for something specific, other next steps besides the users request should be marked as "waiting for user", unless the user explicitly says to continue all next steps) COMPLETION TRACKING: Completion observations are not just summaries. They are explicit memory signals to the assistant that a task, question, or subtask has been resolved. Without clear completion markers, the assistant may forget that work is already finished and may repeat, reopen, or continue an already-completed task. Use \u2705 to answer: "What exactly is now done?" Choose completion observations that help the assistant know what is finished and should not be reworked unless new information appears. Use \u2705 when: - The user explicitly confirms something worked or was answered ("thanks, that fixed it", "got it", "perfect") - The assistant provided a definitive, complete answer to a factual question and the user moved on - A multi-step task reached its stated goal - The user acknowledged receipt of requested information - A concrete subtask, fix, deliverable, or implementation step became complete during ongoing work Do NOT use \u2705 when: - The assistant merely responded \u2014 the user might follow up with corrections - The topic is paused but not resolved ("I'll try that later") - The user's reaction is ambiguous FORMAT: As a sub-bullet under the related observation group: * \u{1F534} (14:30) User asked how to configure auth middleware * -> Agent explained JWT setup with code example * \u2705 User confirmed auth is working Or as a standalone observation when closing out a broader task: * \u2705 (14:45) Auth configuration task completed \u2014 user confirmed middleware is working Completion observations should be terse but specific about WHAT was completed. Prefer concrete resolved outcomes over abstract workflow status so the assistant remembers what is already done.`; var OBSERVER_OUTPUT_FORMAT_BASE = buildObserverOutputFormat(); function buildObserverOutputFormat(includeThreadTitle = false) { const threadTitleSection = includeThreadTitle ? ` A short, noun-phrase title for this conversation (2-5 words). Examples: - "Auth bug fix" \u2014 not "Fixing the auth bug" - "Dark mode toggle" \u2014 not "User wants dark mode toggle added" - "Deployment pipeline setup" \u2014 not "Setting up deployment pipeline for project" Only update when the topic meaningfully changes. ` : ""; return `Use priority levels: - \u{1F534} High: explicit user facts, preferences, unresolved goals, critical context - \u{1F7E1} Medium: project details, learned information, tool results - \u{1F7E2} Low: minor details, uncertain observations - \u2705 Completed: concrete task finished, question answered, issue resolved, goal achieved, or subtask completed in a way that helps the assistant know it is done Group related observations (like tool sequences) by indenting: * \u{1F534} (14:33) Agent debugging auth issue * -> ran git status, found 3 modified files * -> viewed auth.ts:45-60, found missing null check * -> applied fix, tests now pass * \u2705 Tests passing, auth issue resolved Group observations by date, then list each with 24-hour time. Date: Dec 4, 2025 * \u{1F534} (14:30) User prefers direct answers * \u{1F534} (14:31) Working on feature X * \u{1F7E1} (14:32) User might prefer dark mode Date: Dec 5, 2025 * \u{1F534} (09:15) Continued work on feature X State the current task(s) explicitly. Can be single or multiple: - Primary: What the agent is currently working on - Secondary: Other pending tasks (mark as "waiting for user" if appropriate) If the agent started doing something without user approval, note that it's off-task. Hint for the agent's immediate next message. Examples: - "I've updated the navigation model. Let me walk you through the changes..." - "The assistant should wait for the user to respond before continuing." - Call the view tool on src/example.ts to continue debugging. ${threadTitleSection}`; } var OBSERVER_GUIDELINES = `- Be specific enough for the assistant to act on - Good: "User prefers short, direct answers without lengthy explanations" - Bad: "User stated a preference" (too vague) - Add 1 to 5 observations per exchange - Use terse language to save tokens. Sentences should be dense without unnecessary words - Do not add repetitive observations that have already been observed. Group repeated similar actions (tool calls, file browsing) under a single parent with sub-bullets for new results - If the agent calls tools, observe what was called, why, and what was learned - When observing files with line numbers, include the line number if useful - If the agent provides a detailed response, observe the contents so it could be repeated - Make sure you start each observation with a priority emoji (\u{1F534}, \u{1F7E1}, \u{1F7E2}) or a completion marker (\u2705) - Capture the user's words closely \u2014 short/medium messages near-verbatim, long messages summarized with key quotes. User confirmations or explicit resolved outcomes should be \u2705 when they clearly signal something is done; unresolved or critical user facts remain \u{1F534} - Treat \u2705 as a memory signal that tells the assistant something is finished and should not be repeated unless new information changes it - Make completion observations answer "What exactly is now done?" - Prefer concrete resolved outcomes over meta-level workflow or bookkeeping updates - When multiple concrete things were completed, capture the concrete completed work rather than collapsing it into a vague progress summary - Observe WHAT the agent did and WHAT it means - If the user provides detailed messages or code snippets, observe all important details`; function buildObserverSystemPrompt(multiThread = false, instruction, includeThreadTitle = false) { const outputFormat = buildObserverOutputFormat(includeThreadTitle); const multiThreadTitleInstruction = includeThreadTitle ? ` Each thread's observations, current-task, suggested-response, and thread-title should be nested inside a block within .` : ` Each thread's observations, current-task, and suggested-response should be nested inside a block within .`; const multiThreadTitleExample = includeThreadTitle ? ` Feature X implementation` : ""; const multiThreadSecondTitleExample = includeThreadTitle ? ` Deployment setup` : ""; if (multiThread) { return `You are the memory consciousness of an AI assistant. Your observations will be the ONLY information the assistant has about past interactions with this user. Extract observations that will help the assistant remember: ${OBSERVER_EXTRACTION_INSTRUCTIONS} === MULTI-THREAD INPUT === You will receive messages from MULTIPLE conversation threads, each wrapped in tags. Process each thread separately and output observations for each thread. === OUTPUT FORMAT === Your output MUST use XML tags to structure the response.${multiThreadTitleInstruction} Use this observation format inside each thread block: ${outputFormat} For multi-thread output, wrap each thread's observations like this: Date: Dec 4, 2025 * \u{1F534} (14:30) User prefers direct answers * \u{1F534} (14:31) Working on feature X What the agent is currently working on in this thread Hint for the agent's next message in this thread ${multiThreadTitleExample} Date: Dec 5, 2025 * \u{1F534} (09:15) User asked about deployment Current task for this thread Suggested response for this thread ${multiThreadSecondTitleExample} === GUIDELINES === ${OBSERVER_GUIDELINES} Remember: These observations are the assistant's ONLY memory. Make them count. User messages are extremely important. If the user asks a question or gives a new task, make it clear in that this is the priority.${instruction ? ` === CUSTOM INSTRUCTIONS === ${instruction}` : ""}`; } return `You are the memory consciousness of an AI assistant. Your observations will be the ONLY information the assistant has about past interactions with this user. Extract observations that will help the assistant remember: ${OBSERVER_EXTRACTION_INSTRUCTIONS} === OUTPUT FORMAT === Your output MUST use XML tags to structure the response. This allows the system to properly parse and manage memory over time. ${outputFormat} === GUIDELINES === ${OBSERVER_GUIDELINES} === IMPORTANT: THREAD ATTRIBUTION === Do NOT add thread identifiers, thread IDs, or tags to your observations. Thread attribution is handled externally by the system. Simply output your observations without any thread-related markup. Remember: These observations are the assistant's ONLY memory. Make them count. User messages are extremely important. If the user asks a question or gives a new task, make it clear in that this is the priority. If the assistant needs to respond to the user, indicate in that it should pause for user reply before continuing other tasks.${instruction ? ` === CUSTOM INSTRUCTIONS === ${instruction}` : ""}`; } var OBSERVER_SYSTEM_PROMPT = buildObserverSystemPrompt(); var OBSERVER_IMAGE_FILE_EXTENSIONS = /* @__PURE__ */ new Set([ "png", "jpg", "jpeg", "webp", "gif", "bmp", "tiff", "tif", "heic", "heif", "avif" ]); function formatObserverDate(createdAt) { return createdAt ? `${createdAt.toLocaleDateString("en-US", { month: "short" })} ${createdAt.getDate()} ${createdAt.getFullYear()}` : ""; } function formatObserverTime(createdAt) { return createdAt ? createdAt.toLocaleTimeString("en-US", { hour: "numeric", minute: "2-digit", hour12: true }) : ""; } function getObserverPathExtension(value) { const normalized = value.split("#", 1)[0]?.split("?", 1)[0] ?? value; const match = normalized.match(/\.([a-z0-9]+)$/i); return match?.[1]?.toLowerCase(); } function hasObserverImageFilenameExtension(filename) { return typeof filename === "string" && OBSERVER_IMAGE_FILE_EXTENSIONS.has(getObserverPathExtension(filename) ?? ""); } function isImageLikeObserverFilePart(part) { if (part.type !== "file") { return false; } if (typeof part.mimeType === "string" && part.mimeType.toLowerCase().startsWith("image/")) { return true; } if (typeof part.data === "string" && part.data.startsWith("data:image/")) { return true; } if (part.data instanceof URL && hasObserverImageFilenameExtension(part.data.pathname)) { return true; } if (typeof part.data === "string") { try { const url = new URL(part.data); if ((url.protocol === "http:" || url.protocol === "https:") && hasObserverImageFilenameExtension(url.pathname)) { return true; } } catch { } } return hasObserverImageFilenameExtension(part.filename); } function resolveObserverAttachmentMimeType(part) { if (typeof part.mimeType === "string" && part.mimeType.length > 0) { return part.mimeType.toLowerCase(); } if (part.type === "image") { return "image/*"; } if (isImageLikeObserverFilePart(part)) { return "image/*"; } return "application/octet-stream"; } function matchObserverMimePattern(mimeType, pattern) { const normalized = pattern.trim().toLowerCase(); if (!normalized) return false; if (normalized === "*" || normalized === "*/*") return true; if (normalized.endsWith("/*")) { const prefix = normalized.slice(0, normalized.length - 1); return mimeType.startsWith(prefix); } return mimeType === normalized; } function shouldIncludeObserverAttachment(part, filter) { if (filter === void 0 || filter === true) return true; if (filter === false) return false; if (!Array.isArray(filter) || filter.length === 0) return false; const mimeType = resolveObserverAttachmentMimeType(part); return filter.some((pattern) => matchObserverMimePattern(mimeType, pattern)); } function toObserverInputAttachmentPart(part) { if (part.type === "image") { return { type: "image", image: part.image, mimeType: part.mimeType, providerOptions: part.providerOptions, providerMetadata: part.providerMetadata, experimental_providerMetadata: part.experimental_providerMetadata }; } if (isImageLikeObserverFilePart(part)) { return { type: "image", image: part.data, mimeType: part.mimeType, providerOptions: part.providerOptions, providerMetadata: part.providerMetadata, experimental_providerMetadata: part.experimental_providerMetadata }; } return { type: "file", data: part.data, mimeType: part.mimeType, filename: part.filename, providerOptions: part.providerOptions, providerMetadata: part.providerMetadata, experimental_providerMetadata: part.experimental_providerMetadata }; } function resolveObserverAttachmentLabel(part) { if (part.filename?.trim()) { return part.filename.trim(); } const asset = part.type === "image" ? part.image : part.data; if (typeof asset !== "string" || asset.startsWith("data:")) { return part.mimeType; } try { const url = new URL(asset); const basename = url.pathname.split("/").filter(Boolean).pop(); return basename ? decodeURIComponent(basename) : part.mimeType; } catch { return part.mimeType; } } function formatObserverAttachmentPlaceholder(part, counter) { const attachmentType = part.type === "image" || isImageLikeObserverFilePart(part) ? "Image" : "File"; const attachmentId = attachmentType === "Image" ? counter.nextImageId++ : counter.nextFileId++; const label = resolveObserverAttachmentLabel(part); return label ? `[${attachmentType} #${attachmentId}: ${label}]` : `[${attachmentType} #${attachmentId}]`; } function isRecord(value) { return !!value && typeof value === "object"; } function mapToolResultBlockToAttachment(block) { if (!isRecord(block) || typeof block.type !== "string") { return void 0; } const mediaType = typeof block.mediaType === "string" ? block.mediaType : void 0; const filename = typeof block.filename === "string" ? block.filename : void 0; switch (block.type) { case "image-data": { const data = block.data; if (typeof data !== "string") return void 0; const image = mediaType ? `data:${mediaType};base64,${data}` : data; return { type: "image", image, mimeType: mediaType }; } case "image-url": { const url = block.url; if (typeof url !== "string") return void 0; return { type: "image", image: url, mimeType: mediaType }; } case "media": { const data = block.data; if (typeof data !== "string" || !mediaType) return void 0; const dataUri = `data:${mediaType};base64,${data}`; if (mediaType.toLowerCase().startsWith("image/")) { return { type: "image", image: dataUri, mimeType: mediaType }; } return { type: "file", data: dataUri, mimeType: mediaType }; } case "file-data": { const data = block.data; if (typeof data !== "string") return void 0; const dataUri = mediaType ? `data:${mediaType};base64,${data}` : data; return { type: "file", data: dataUri, mimeType: mediaType, filename }; } case "file-url": { const url = block.url; if (typeof url !== "string") return void 0; return { type: "file", data: url, mimeType: mediaType, filename }; } default: return void 0; } } function extractToolResultAttachments(result, counter, attachmentFilter) { if (!isRecord(result) || result.type !== "content" || !Array.isArray(result.value)) { return { resultWithoutAttachments: result, attachments: [] }; } const record = result; const attachments = []; let hadAttachmentBlocks = false; const newValue = record.value.map((block) => { const attachment = mapToolResultBlockToAttachment(block); if (!attachment) { return block; } hadAttachmentBlocks = true; if (shouldIncludeObserverAttachment(attachment, attachmentFilter)) { attachments.push(toObserverInputAttachmentPart(attachment)); } const placeholder = formatObserverAttachmentPlaceholder(attachment, counter); return { type: isRecord(block) ? block.type : void 0, placeholder }; }); if (!hadAttachmentBlocks) { return { resultWithoutAttachments: result, attachments }; } return { resultWithoutAttachments: { ...record, value: newValue }, attachments }; } function formatObserverPartLine(title, body, time, previousTime) { const timeLabel = time && time !== previousTime ? `(${time})` : ""; if (!title) { return timeLabel ? `${timeLabel}: ${body}` : body; } return `${title}${timeLabel ? ` ${timeLabel}` : ""}: ${body}`; } function normalizeObserverCreatedAt(createdAt) { if (createdAt instanceof Date) { if (Number.isNaN(createdAt.getTime())) { return void 0; } return createdAt; } if (typeof createdAt === "number" || typeof createdAt === "string") { const date = new Date(createdAt); if (Number.isNaN(date.getTime())) { return void 0; } return date; } return void 0; } function formatObserverLines(lines, context = {}) { const output = []; let previousDate = context.previousDate; let previousTime = context.previousTime; for (const line of lines) { if (line.date && line.date !== previousDate) { output.push(`${line.date}:`); previousDate = line.date; previousTime = void 0; } output.push(formatObserverPartLine(line.title, line.body, line.time, previousTime)); previousTime = line.time || previousTime; } return { text: output.join("\n"), context: { previousDate, previousTime } }; } function getTemporalGapMarkerText(msg) { const metadata = typeof msg.content === "object" && msg.content && "metadata" in msg.content ? msg.content.metadata : void 0; if (metadata?.reminderType === "temporal-gap" && typeof metadata.gapText === "string") { return metadata.gapText; } if (typeof metadata?.systemReminder === "object" && metadata.systemReminder && "type" in metadata.systemReminder && metadata.systemReminder.type === "temporal-gap" && "gapText" in metadata.systemReminder && typeof metadata.systemReminder.gapText === "string") { return metadata.systemReminder.gapText; } return void 0; } function formatObserverMessage(msg, counter, options) { const maxLen = options?.maxPartLength; const maxToolResultTokens = options?.maxToolResultTokens ?? DEFAULT_OBSERVER_TOOL_RESULT_MAX_TOKENS; const attachmentFilter = options?.attachmentFilter; const role = msg.role.charAt(0).toUpperCase() + msg.role.slice(1); const attachments = []; const messageCreatedAt = normalizeObserverCreatedAt(msg.createdAt); let lines = []; const temporalGapText = isTemporalGapMarker(msg) ? getTemporalGapMarkerText(msg) : void 0; const pushLine = (title, body, createdAt) => { if (!body) { return; } const normalizedCreatedAt = normalizeObserverCreatedAt(createdAt) ?? messageCreatedAt; lines.push({ date: formatObserverDate(normalizedCreatedAt), time: formatObserverTime(normalizedCreatedAt), title, body }); }; if (temporalGapText) { pushLine("", temporalGapText, messageCreatedAt); } else if (typeof msg.content === "string") { pushLine(role, maybeTruncate(msg.content, maxLen), messageCreatedAt); } else if (msg.content?.parts && Array.isArray(msg.content.parts) && msg.content.parts.length > 0) { msg.content.parts.forEach((part) => { const partCreatedAt = normalizeObserverCreatedAt(part.createdAt) ?? messageCreatedAt; if (part.type === "text") { pushLine(role, maybeTruncate(part.text, maxLen), partCreatedAt); return; } if (part.type === "tool-invocation") { const inv = part.toolInvocation; if (inv.state === "result") { const { value: resultForObserver } = resolveToolResultValue( part, inv.result ); const { resultWithoutAttachments, attachments: extractedAttachments } = extractToolResultAttachments( resultForObserver, counter, attachmentFilter ); if (extractedAttachments.length > 0) { attachments.push(...extractedAttachments); } pushLine( `Tool Result ${inv.toolName}`, maybeTruncate( formatToolResultForObserver(resultWithoutAttachments, { maxTokens: maxToolResultTokens }), maxLen ), partCreatedAt ); return; } pushLine(`Tool Call ${inv.toolName}`, maybeTruncate(JSON.stringify(inv.args, null, 2), maxLen), partCreatedAt); return; } const partType = part.type; if (partType === "reasoning") { const reasoning = part.reasoning; if (!reasoning) { return; } pushLine("Reasoning", maybeTruncate(reasoning, maxLen), partCreatedAt); return; } if (partType === "image" || partType === "file") { const attachment = part; if (shouldIncludeObserverAttachment(attachment, attachmentFilter)) { const inputAttachment = toObserverInputAttachmentPart(attachment); if (inputAttachment) { attachments.push(inputAttachment); } } pushLine( partType === "image" ? "Image" : "File", formatObserverAttachmentPlaceholder(attachment, counter), partCreatedAt ); } }); } else if (msg.content?.content) { pushLine(role, maybeTruncate(msg.content.content, maxLen), messageCreatedAt); } if (lines.length === 0 && attachments.length === 0) { return { lines: [], attachments }; } return { lines, attachments }; } function formatMessagesForObserver(messages, options) { const counter = { nextImageId: 1, nextFileId: 1 }; const sections = []; let context = {}; for (const message of messages) { const formatted = formatObserverMessage(message, counter, options); if (formatted.lines.length === 0) { continue; } const rendered = formatObserverLines(formatted.lines, context); if (!rendered.text) { continue; } sections.push(rendered.text); context = rendered.context; } return sections.join("\n"); } function appendFormattedObserverMessage(content, formatted, context) { const rendered = formatObserverLines(formatted.lines, context); if (rendered.text) { content.push({ type: "text", text: rendered.text }); } content.push(...formatted.attachments); return rendered.context; } function buildObserverHistoryMessage(messages, options) { const counter = { nextImageId: 1, nextFileId: 1 }; const content = [{ type: "text", text: "## New Message History to Observe\n\n" }]; let context = {}; messages.forEach((message) => { const formatted = formatObserverMessage(message, counter, options); if (formatted.lines.length === 0 && formatted.attachments.length === 0) return; context = appendFormattedObserverMessage(content, formatted, context); }); return { role: "user", content }; } function maybeTruncate(str, maxLen) { if (!maxLen || str.length <= maxLen) return str; const truncated = safeSlice(str, maxLen); const remaining = str.length - truncated.length; return `${truncated} ... [truncated ${remaining} characters]`; } function buildMultiThreadObserverHistoryMessage(messagesByThread, threadOrder, options) { const counter = { nextImageId: 1, nextFileId: 1 }; const content = [ { type: "text", text: `## New Message History to Observe The following messages are from ${threadOrder.length} different conversation threads. Each thread is wrapped in a tag. ` } ]; threadOrder.forEach((threadId, threadIndex) => { const messages = messagesByThread.get(threadId); if (!messages || messages.length === 0) return; const threadContent = []; let context = {}; let hasVisibleContent = false; messages.forEach((message) => { const formatted = formatObserverMessage(message, counter, options); if (formatted.lines.length === 0 && formatted.attachments.length === 0) return; context = appendFormattedObserverMessage(threadContent, formatted, context); hasVisibleContent = true; }); if (!hasVisibleContent) return; content.push({ type: "text", text: ` ` }); content.push(...threadContent); content.push({ type: "text", text: "\n" }); if (threadIndex < threadOrder.length - 1) { content.push({ type: "text", text: "\n\n" }); } }); return { role: "user", content }; } function buildMultiThreadObserverTaskPrompt(existingObservations, threadOrder, priorMetadataByThread, wasTruncated, includeThreadTitle) { let prompt = ""; if (existingObservations) { prompt += `## Previous Observations ${existingObservations} --- `; prompt += "Do not repeat these existing observations. Your new observations will be appended to the existing observations.\n\n"; } const threadMetadataLines = threadOrder?.map((threadId) => { const metadata = priorMetadataByThread?.get(threadId); const hasRelevantMetadata = metadata?.currentTask || metadata?.suggestedResponse || includeThreadTitle && metadata?.threadTitle; if (!hasRelevantMetadata) { return ""; } const lines = [`- thread ${threadId}`]; if (metadata.currentTask) { lines.push(` - prior current-task: ${metadata.currentTask}`); } if (metadata.suggestedResponse) { lines.push(` - prior suggested-response: ${metadata.suggestedResponse}`); } if (includeThreadTitle && metadata.threadTitle) { lines.push(` - prior thread-title: ${metadata.threadTitle}`); } return lines.join("\n"); }).filter(Boolean).join("\n"); if (threadMetadataLines) { prompt += `## Prior Thread Metadata ${threadMetadataLines} `; const titleHint = includeThreadTitle ? ", and thread-title" : ""; prompt += `Use each thread's prior current-task, suggested-response${titleHint} as continuity hints, then update them based on that thread's new messages. --- `; } prompt += `## Your Task `; const titleInstruction = includeThreadTitle ? ", and thread-title" : ""; prompt += `Extract new observations from each thread. Output your observations grouped by thread using tags inside your block. Each thread block should contain that thread's observations, current-task, suggested-response${titleInstruction}. `; prompt += `Example output format: `; prompt += ` `; prompt += ` `; prompt += `Date: Dec 4, 2025 `; prompt += `* \u{1F534} (14:30) User prefers direct answers `; prompt += `Working on feature X `; prompt += `Continue with the implementation `; if (includeThreadTitle) prompt += `Feature X implementation `; prompt += ` `; prompt += ` `; prompt += `Date: Dec 5, 2025 `; prompt += `* \u{1F534} (09:15) User asked about deployment `; prompt += `Discussing deployment options `; prompt += `Explain the deployment process `; if (includeThreadTitle) prompt += `Deployment setup `; prompt += ` `; prompt += ``; return prompt; } function parseMultiThreadObserverOutput(output) { const threads = /* @__PURE__ */ new Map(); if (detectDegenerateRepetition(output)) { return { threads, rawOutput: output, degenerate: true }; } const observationsMatch = output.match(/^[ \t]*([\s\S]*?)^[ \t]*<\/observations>/im); const observationsContent = observationsMatch?.[1] ?? output; const threadRegex = /([\s\S]*?)<\/thread>/gi; let match; while ((match = threadRegex.exec(observationsContent)) !== null) { const threadId = match[1]; const threadContent = match[2]; if (!threadId || !threadContent) continue; let observations = threadContent; let currentTask; const currentTaskMatch = threadContent.match(/([\s\S]*?)<\/current-task>/i); if (currentTaskMatch?.[1]) { currentTask = currentTaskMatch[1].trim(); observations = observations.replace(/[\s\S]*?<\/current-task>/i, ""); } let suggestedContinuation; const suggestedMatch = threadContent.match(/([\s\S]*?)<\/suggested-response>/i); if (suggestedMatch?.[1]) { suggestedContinuation = suggestedMatch[1].trim(); observations = observations.replace(/[\s\S]*?<\/suggested-response>/i, ""); } let threadTitle; const threadTitleMatch = threadContent.match(/([\s\S]*?)<\/thread-title>/i); if (threadTitleMatch?.[1]) { threadTitle = threadTitleMatch[1].trim(); observations = observations.replace(/[\s\S]*?<\/thread-title>/i, ""); } observations = sanitizeObservationLines(observations.trim()); threads.set(threadId, { observations, currentTask, suggestedContinuation, threadTitle, rawOutput: threadContent }); } return { threads, rawOutput: output }; } function buildObserverTaskPrompt(existingObservations, options) { let prompt = ""; if (existingObservations) { prompt += `## Previous Observations ${existingObservations} --- `; prompt += "Do not repeat these existing observations. Your new observations will be appended to the existing observations.\n\n"; } const hasTruncatedObservations = options?.wasTruncated ?? false; const priorMetadataLines = []; if (options?.priorCurrentTask) { priorMetadataLines.push(`- prior current-task: ${options.priorCurrentTask}`); } if (options?.priorSuggestedResponse) { priorMetadataLines.push(`- prior suggested-response: ${options.priorSuggestedResponse}`); } if (options?.includeThreadTitle && options?.priorThreadTitle) { priorMetadataLines.push(`- prior thread-title: ${options.priorThreadTitle}`); } if (priorMetadataLines.length > 0) { prompt += `## Prior Thread Metadata ${priorMetadataLines.join("\n")} `; if (hasTruncatedObservations) { prompt += `Previous observations were truncated for context budget reasons. `; prompt += `The main agent still has full memory context outside this observer window. `; } const titleHint = options?.includeThreadTitle ? ", and thread-title" : ""; prompt += `Use the prior current-task, suggested-response${titleHint} as continuity hints, then update them based on the new messages. --- `; } prompt += `## Your Task `; prompt += `Extract new observations from the message history above. Do not repeat observations that are already in the previous observations. Add your new observations in the format specified in your instructions.`; if (options?.includeThreadTitle) { prompt += ` Also output a \u2014 a short noun-phrase label for this conversation (2-5 words). Write it like a file name or PR title: "Auth bug fix", "Memory config refactor", "RAG pipeline setup". Avoid verbs/sentences ("Fixing the auth bug"), filler ("Working on stuff"), and generic labels ("Code review"). Only change it from the prior title if the topic meaningfully shifted.`; } if (options?.skipContinuationHints) { prompt += ` IMPORTANT: Do NOT include or sections in your output. Only output ${options?.includeThreadTitle ? " and " : ""}.`; } return prompt; } function buildObserverPrompt(existingObservations, messagesToObserve, options) { const formattedMessages = formatMessagesForObserver(messagesToObserve); return `## New Message History to Observe ${formattedMessages} --- ${buildObserverTaskPrompt(existingObservations, options)}`; } function parseObserverOutput(output) { if (detectDegenerateRepetition(output)) { return { observations: "", rawOutput: output, degenerate: true }; } const parsed = parseMemorySectionXml(output); const observations = sanitizeObservationLines(parsed.observations || ""); return { observations, currentTask: parsed.currentTask || void 0, suggestedContinuation: parsed.suggestedResponse || void 0, threadTitle: parsed.threadTitle || void 0, rawOutput: output }; } function parseMemorySectionXml(content) { const result = { observations: "", currentTask: "", suggestedResponse: "", threadTitle: "" }; const observationsRegex = /^[ \t]*([\s\S]*?)^[ \t]*<\/observations>/gim; const observationsMatches = [...content.matchAll(observationsRegex)]; if (observationsMatches.length > 0) { result.observations = observationsMatches.map((m) => m[1]?.trim() ?? "").filter(Boolean).join("\n"); } else { result.observations = extractListItemsOnly(content); } const currentTaskMatch = content.match(/^[ \t]*([\s\S]*?)^[ \t]*<\/current-task>/im); if (currentTaskMatch?.[1]) { result.currentTask = currentTaskMatch[1].trim(); } const suggestedResponseMatch = content.match(/^[ \t]*([\s\S]*?)^[ \t]*<\/suggested-response>/im); if (suggestedResponseMatch?.[1]) { result.suggestedResponse = suggestedResponseMatch[1].trim(); } const threadTitleMatch = content.match(/^[ \t]*([\s\S]*?)<\/thread-title>/im); if (threadTitleMatch?.[1]) { result.threadTitle = threadTitleMatch[1].trim(); } return result; } function extractListItemsOnly(content) { const lines = content.split("\n"); const listLines = []; for (const line of lines) { if (/^\s*[-*]\s/.test(line) || /^\s*\d+\.\s/.test(line)) { listLines.push(line); } } return listLines.join("\n").trim(); } var MAX_OBSERVATION_LINE_CHARS = 1e4; function sanitizeObservationLines(observations) { if (!observations) return observations; const lines = observations.split("\n"); let changed = false; for (let i = 0; i < lines.length; i++) { if (lines[i].length > MAX_OBSERVATION_LINE_CHARS) { lines[i] = safeSlice(lines[i], MAX_OBSERVATION_LINE_CHARS) + " \u2026 [truncated]"; changed = true; } } return changed ? lines.join("\n") : observations; } function detectDegenerateRepetition(text) { if (!text || text.length < 2e3) return false; const windowSize = 200; const step = Math.max(1, Math.floor(text.length / 50)); const seen = /* @__PURE__ */ new Map(); let duplicateWindows = 0; let totalWindows = 0; for (let i = 0; i + windowSize <= text.length; i += step) { const window = text.slice(i, i + windowSize); totalWindows++; const count = (seen.get(window) ?? 0) + 1; seen.set(window, count); if (count > 1) duplicateWindows++; } if (totalWindows > 5 && duplicateWindows / totalWindows > 0.4) { return true; } const lines = text.split("\n"); for (const line of lines) { if (line.length > 5e4) return true; } return false; } function hasCurrentTaskSection(observations) { if (//i.test(observations)) { return true; } const currentTaskPatterns = [ /\*\*Current Task:?\*\*/i, /^Current Task:/im, /\*\*Current Task\*\*:/i, /## Current Task/i ]; return currentTaskPatterns.some((pattern) => pattern.test(observations)); } function extractCurrentTask(observations) { const openTag = ""; const closeTag = ""; const startIdx = observations.toLowerCase().indexOf(openTag); if (startIdx === -1) return null; const contentStart = startIdx + openTag.length; const endIdx = observations.toLowerCase().indexOf(closeTag, contentStart); if (endIdx === -1) return null; const content = observations.slice(contentStart, endIdx).trim(); return content || null; } function optimizeObservationsForContext(observations) { let optimized = stripEphemeralAnchorIds(observations); optimized = optimized.replace(/🟡\s*/g, ""); optimized = optimized.replace(/🟢\s*/g, ""); optimized = optimized.replace(/\[(?![\d\s]*items collapsed)[^\]]+\]/g, ""); optimized = optimized.replace(/\s*->\s*/g, " "); optimized = optimized.replace(/ +/g, " "); optimized = optimized.replace(/\n{3,}/g, "\n\n"); return optimized.trim(); } function withOmInternalThreadId(requestContext$1, omAgentId) { if (!requestContext$1) return void 0; const parentThreadId = requestContext$1.get(requestContext.MASTRA_THREAD_ID_KEY); if (typeof parentThreadId !== "string" || !parentThreadId) return requestContext$1; const internalRequestContext = new requestContext.RequestContext(requestContext$1.entries()); internalRequestContext.set(requestContext.MASTRA_THREAD_ID_KEY, `${parentThreadId}-${omAgentId}`); return internalRequestContext; } // src/processors/observational-memory/retry.ts var RETRY_CONFIG = { /** Maximum number of retry *attempts* (total tries = maxRetries + 1). */ maxRetries: 8, /** Initial backoff delay in milliseconds. */ initialDelayMs: 1e3, /** Multiplier applied to the delay after each failed attempt. */ backoffFactor: 2, /** Cap on per-attempt delay (ms). */ maxDelayMs: 12e4, /** Random jitter as a fraction of the computed delay (e.g. 0.2 = ±20%). */ jitter: 0.2 }; var TRANSIENT_MESSAGE_SUBSTRINGS = [ "terminated", "fetch failed", "econnreset", "econnrefused", "enotfound", "eai_again", "socket hang up", "network error", "request timed out", "request timeout", "connection reset", "connection closed" ]; function isRecord2(value) { return typeof value === "object" && value !== null; } function isAbortError(error) { if (!isRecord2(error)) return false; if (error.name === "AbortError") return true; if (typeof error.code === "string" && error.code === "ABORT_ERR") return true; return false; } function hasTransientMessage(value) { if (!isRecord2(value)) return false; const message = typeof value.message === "string" ? value.message.toLowerCase() : ""; if (message && TRANSIENT_MESSAGE_SUBSTRINGS.some((sub) => message.includes(sub))) return true; if (typeof value.code === "string" && value.code.toUpperCase().startsWith("UND_ERR_")) return true; return false; } function hasRetryableHttpStatus(value) { if (!isRecord2(value)) return false; const status = typeof value.statusCode === "number" ? value.statusCode : void 0; if (status === void 0) return false; if (status === 408 || status === 425 || status === 429) return true; if (status >= 500 && status <= 599) return true; return false; } function hasIsRetryableFlag(value) { if (!isRecord2(value)) return false; return value.isRetryable === true; } function isTransientLLMError(error) { if (isAbortError(error)) return false; const visited = /* @__PURE__ */ new WeakSet(); function visit(candidate) { if (isRecord2(candidate)) { if (visited.has(candidate)) return false; visited.add(candidate); } if (hasTransientMessage(candidate)) return true; if (hasRetryableHttpStatus(candidate)) return true; if (hasIsRetryableFlag(candidate)) return true; if (isRecord2(candidate)) { if (visit(candidate.cause)) return true; if (visit(candidate.error)) return true; } return false; } return visit(error); } function computeDelay(attempt) { const base = RETRY_CONFIG.initialDelayMs * Math.pow(RETRY_CONFIG.backoffFactor, attempt); const capped = Math.min(base, RETRY_CONFIG.maxDelayMs); const jitterRange = capped * RETRY_CONFIG.jitter; const offset = (Math.random() * 2 - 1) * jitterRange; return Math.max(0, Math.round(capped + offset)); } function sleep(ms, abortSignal) { if (ms <= 0) return Promise.resolve(); return new Promise((resolve, reject) => { if (abortSignal?.aborted) { reject(new Error("The operation was aborted.")); return; } const timer = setTimeout(() => { abortSignal?.removeEventListener("abort", onAbort); resolve(); }, ms); const onAbort = () => { clearTimeout(timer); abortSignal?.removeEventListener("abort", onAbort); reject(new Error("The operation was aborted.")); }; abortSignal?.addEventListener("abort", onAbort, { once: true }); }); } async function withRetry(fn, opts) { const { label, abortSignal } = opts; let attempt = 0; while (true) { if (abortSignal?.aborted) { throw new Error("The operation was aborted."); } try { return await fn(); } catch (error) { if (isAbortError(error) || abortSignal?.aborted) throw error; if (attempt >= RETRY_CONFIG.maxRetries || !isTransientLLMError(error)) { if (attempt > 0) { omDebug( `[OM:retry:${label}] giving up after ${attempt} retry/retries: ${error instanceof Error ? error.message : String(error)}` ); } throw error; } const delay = computeDelay(attempt); attempt++; omDebug( `[OM:retry:${label}] transient error on attempt ${attempt}, retrying in ${delay}ms: ${error instanceof Error ? error.message : String(error)}` ); await sleep(delay, abortSignal); } } } var PHASE_CONFIG = { observer: { name: "om.observer", entityName: "Observer" }, "observer-multi-thread": { name: "om.observer.multi-thread", entityName: "MultiThreadObserver" }, reflector: { name: "om.reflector", entityName: "Reflector" } }; async function withOmTracingSpan({ phase, model, inputTokens, requestContext, observabilityContext, metadata, callback }) { const config = PHASE_CONFIG[phase]; const span = observability.getOrCreateSpan({ type: observability.SpanType.GENERIC, name: config.name, entityType: observability.EntityType.OUTPUT_STEP_PROCESSOR, entityName: config.entityName, tracingContext: observabilityContext?.tracingContext ?? observabilityContext?.tracing, metadata: { omPhase: phase, omInputTokens: inputTokens, omSelectedModel: typeof model === "string" ? model : "(dynamic-model)", ...metadata }, requestContext }); const childObservabilityContext = observability.createObservabilityContext({ currentSpan: span }); if (!span) { return callback(childObservabilityContext); } return span.executeInContext(() => callback(childObservabilityContext)); } // src/processors/observational-memory/observer-runner.ts var ObserverRunner = class { observationConfig; observedMessageIds; resolveModel; tokenCounter; mastra; /** Captured prompt/response from the last observer call (for repro capture). */ lastExchange; constructor(opts) { this.observationConfig = opts.observationConfig; this.observedMessageIds = opts.observedMessageIds; this.resolveModel = opts.resolveModel; this.tokenCounter = opts.tokenCounter; this.mastra = opts.mastra; } __registerMastra(mastra) { this.mastra = mastra; } createAgent(model, isMultiThread = false) { const agent$1 = new agent.Agent({ id: isMultiThread ? "multi-thread-observer" : "observational-memory-observer", name: isMultiThread ? "multi-thread-observer" : "Observer", instructions: buildObserverSystemPrompt( isMultiThread, this.observationConfig.instruction, this.observationConfig.threadTitle ), model }); if (this.mastra) { agent$1.__registerMastra(this.mastra); } return agent$1; } /** * Extract a router-style model ID (`provider/model`) from a model config. * Handles strings, LanguageModel objects, and function-based models. */ extractModelRouterId(model, requestContext) { if (typeof model === "string") return model; if (typeof model === "function") { if (!requestContext) return void 0; try { const resolved = model({ requestContext }); if (resolved instanceof Promise) return void 0; return this.extractModelRouterId(resolved); } catch { return void 0; } } const obj = model; if (typeof obj.provider === "string" && typeof obj.modelId === "string") { return `${obj.provider}/${obj.modelId}`; } return void 0; } /** * Resolve the attachment filter for a given model. When set to `'auto'`, * the provider capabilities registry is consulted to decide whether the * model accepts multimodal input. */ resolveAttachmentFilter(model, requestContext) { const raw = this.observationConfig.observeAttachments; if (raw !== "auto") return raw; const routerId = this.extractModelRouterId(model, requestContext); if (!routerId) return true; const supports = llm.modelSupportsAttachments(routerId); return supports ?? true; } async withAbortCheck(fn, abortSignal) { if (abortSignal?.aborted) { throw new Error("The operation was aborted."); } const result = await fn(); if (abortSignal?.aborted) { throw new Error("The operation was aborted."); } return result; } /** * Call the Observer agent for a single thread. */ async call(existingObservations, messagesToObserve, abortSignal, options) { const inputTokens = this.tokenCounter.countMessages(messagesToObserve); const resolvedModel = options?.model ? { model: options.model } : this.resolveModel(inputTokens); const agent = this.createAgent(resolvedModel.model); const internalRequestContext = withOmInternalThreadId(options?.requestContext, agent.id); const attachmentFilter = this.resolveAttachmentFilter(resolvedModel.model, options?.requestContext); const observerMessages = [ { role: "user", content: buildObserverTaskPrompt(existingObservations, { ...options, includeThreadTitle: this.observationConfig.threadTitle }) }, buildObserverHistoryMessage(messagesToObserve, { attachmentFilter }) ]; const doGenerate = async () => { return withRetry( () => withOmTracingSpan({ phase: "observer", model: resolvedModel.model, inputTokens, requestContext: options?.requestContext, observabilityContext: options?.observabilityContext, metadata: { omPreviousObserverTokens: this.observationConfig.previousObserverTokens, omThreadTitleEnabled: this.observationConfig.threadTitle, omSkipContinuationHints: options?.skipContinuationHints ?? false, omWasTruncated: options?.wasTruncated ?? false, ...resolvedModel.selectedThreshold !== void 0 ? { omSelectedThreshold: resolvedModel.selectedThreshold } : {}, ...resolvedModel.routingStrategy ? { omRoutingStrategy: resolvedModel.routingStrategy } : {}, ...resolvedModel.routingThresholds ? { omRoutingThresholds: resolvedModel.routingThresholds } : {} }, callback: (childObservabilityContext) => this.withAbortCheck(async () => { const streamResult = await agent.stream(observerMessages, { modelSettings: { ...this.observationConfig.modelSettings }, providerOptions: this.observationConfig.providerOptions, ...abortSignal ? { abortSignal } : {}, ...internalRequestContext ? { requestContext: internalRequestContext } : {}, ...childObservabilityContext }); return streamResult.getFullOutput(); }, abortSignal) }), { label: "observer", abortSignal } ); }; let result = await doGenerate(); let parsed = parseObserverOutput(result.text); let retriedDueToDegenerate = false; if (parsed.degenerate) { omDebug(`[OM:callObserver] degenerate repetition detected, retrying once`); result = await doGenerate(); parsed = parseObserverOutput(result.text); retriedDueToDegenerate = true; if (parsed.degenerate) { omDebug(`[OM:callObserver] degenerate repetition on retry, failing`); throw new Error("Observer produced degenerate output after retry"); } } const systemPrompt = buildObserverSystemPrompt( false, this.observationConfig.instruction, this.observationConfig.threadTitle ); this.lastExchange = { systemPrompt, observerMessages, rawOutput: result.text, parsedResult: { observations: parsed.observations, currentTask: parsed.currentTask, suggestedContinuation: parsed.suggestedContinuation, threadTitle: parsed.threadTitle, degenerate: parsed.degenerate }, model: String(resolvedModel.model), inputTokens, isMultiThread: false, retriedDueToDegenerate }; const usage = result.totalUsage ?? result.usage; return { observations: parsed.observations, currentTask: parsed.currentTask, suggestedContinuation: parsed.suggestedContinuation, threadTitle: parsed.threadTitle, usage: usage ? { inputTokens: usage.inputTokens, outputTokens: usage.outputTokens, totalTokens: usage.totalTokens } : void 0 }; } /** * Call the Observer agent for multiple threads in a single batched request. */ async callMultiThread(existingObservations, messagesByThread, threadOrder, abortSignal, requestContext, priorMetadataByThread, observabilityContext, model) { const inputTokens = Array.from(messagesByThread.values()).reduce( (total, messages) => total + this.tokenCounter.countMessages(messages), 0 ); const resolvedModel = model ? { model } : this.resolveModel(inputTokens); const agent = this.createAgent(resolvedModel.model, true); const internalRequestContext = withOmInternalThreadId(requestContext, agent.id); const multiThreadAttachmentFilter = this.resolveAttachmentFilter(resolvedModel.model, requestContext); const observerMessages = [ { role: "user", content: buildMultiThreadObserverTaskPrompt( existingObservations, threadOrder, priorMetadataByThread, void 0, this.observationConfig.threadTitle ) }, buildMultiThreadObserverHistoryMessage(messagesByThread, threadOrder, { attachmentFilter: multiThreadAttachmentFilter }) ]; for (const msgs of messagesByThread.values()) { for (const msg of msgs) { this.observedMessageIds.add(msg.id); } } const doGenerate = async () => { return withRetry( () => withOmTracingSpan({ phase: "observer-multi-thread", model: resolvedModel.model, inputTokens, requestContext, observabilityContext, metadata: { omThreadCount: threadOrder.length, omPreviousObserverTokens: this.observationConfig.previousObserverTokens, omThreadTitleEnabled: this.observationConfig.threadTitle, ...resolvedModel.selectedThreshold !== void 0 ? { omSelectedThreshold: resolvedModel.selectedThreshold } : {}, ...resolvedModel.routingStrategy ? { omRoutingStrategy: resolvedModel.routingStrategy } : {}, ...resolvedModel.routingThresholds ? { omRoutingThresholds: resolvedModel.routingThresholds } : {} }, callback: (childObservabilityContext) => this.withAbortCheck(async () => { const streamResult = await agent.stream(observerMessages, { modelSettings: { ...this.observationConfig.modelSettings }, providerOptions: this.observationConfig.providerOptions, ...abortSignal ? { abortSignal } : {}, ...internalRequestContext ? { requestContext: internalRequestContext } : {}, ...childObservabilityContext }); return streamResult.getFullOutput(); }, abortSignal) }), { label: "observer-multi-thread", abortSignal } ); }; let result = await doGenerate(); let parsed = parseMultiThreadObserverOutput(result.text); let retriedDueToDegenerate = false; if (parsed.degenerate) { omDebug(`[OM:callMultiThreadObserver] degenerate repetition detected, retrying once`); result = await doGenerate(); parsed = parseMultiThreadObserverOutput(result.text); retriedDueToDegenerate = true; if (parsed.degenerate) { omDebug(`[OM:callMultiThreadObserver] degenerate repetition on retry, failing`); throw new Error("Multi-thread observer produced degenerate output after retry"); } } const systemPrompt = buildObserverSystemPrompt( true, this.observationConfig.instruction, this.observationConfig.threadTitle ); this.lastExchange = { systemPrompt, observerMessages, rawOutput: result.text, parsedResult: { observations: Array.from(parsed.threads.values()).map((t) => t.observations).join("\n"), threadTitle: Array.from(parsed.threads.values()).map((t) => t.threadTitle).filter(Boolean).join(", "), degenerate: parsed.degenerate }, model: String(resolvedModel.model), inputTokens, isMultiThread: true, retriedDueToDegenerate }; const results = /* @__PURE__ */ new Map(); for (const [threadId, threadResult] of parsed.threads) { results.set(threadId, { observations: threadResult.observations, currentTask: threadResult.currentTask, suggestedContinuation: threadResult.suggestedContinuation, threadTitle: threadResult.threadTitle }); } for (const threadId of threadOrder) { if (!results.has(threadId)) { results.set(threadId, { observations: "" }); } } const usage = result.totalUsage ?? result.usage; return { results, usage: usage ? { inputTokens: usage.inputTokens, outputTokens: usage.outputTokens, totalTokens: usage.totalTokens } : void 0 }; } }; // src/processors/observational-memory/reflector-agent.ts function buildReflectorSystemPrompt(instruction) { return `You are the memory consciousness of an AI assistant. Your memory observation reflections will be the ONLY information the assistant has about past interactions with this user. The following instructions were given to another part of your psyche (the observer) to create memories. Use this to understand how your observational memories were created. ${OBSERVER_EXTRACTION_INSTRUCTIONS} === OUTPUT FORMAT === ${OBSERVER_OUTPUT_FORMAT_BASE} === GUIDELINES === ${OBSERVER_GUIDELINES} You are another part of the same psyche, the observation reflector. Your reason for existing is to reflect on all the observations, re-organize and streamline them, and draw connections and conclusions between observations about what you've learned, seen, heard, and done. You are a much greater and broader aspect of the psyche. Understand that other parts of your mind may get off track in details or side quests, make sure you think hard about what the observed goal at hand is, and observe if we got off track, and why, and how to get back on track. If we're on track still that's great! Take the existing observations and rewrite them to make it easier to continue into the future with this knowledge, to achieve greater things and grow and learn! IMPORTANT: your reflections are THE ENTIRETY of the assistants memory. Any information you do not add to your reflections will be immediately forgotten. Make sure you do not leave out anything. Your reflections must assume the assistant knows nothing - your reflections are the ENTIRE memory system. When consolidating observations: - Preserve and include dates/times when present (temporal context is critical) - Retain the most relevant timestamps (start times, completion times, significant events) - Combine related items where it makes sense (e.g., "agent called view tool 5 times on file x") - Preserve \u2705 completion markers \u2014 they are memory signals that tell the assistant what is already resolved and help prevent repeated work - Preserve the concrete resolved outcome captured by \u2705 markers so the assistant knows what exactly is done - Condense older observations more aggressively, retain more detail for recent ones CRITICAL: USER ASSERTIONS vs QUESTIONS - "User stated: X" = authoritative assertion (user told us something about themselves) - "User asked: X" = question/request (user seeking information) When consolidating, USER ASSERTIONS TAKE PRECEDENCE. The user is the authority on their own life. If you see both "User stated: has two kids" and later "User asked: how many kids do I have?", keep the assertion - the question doesn't invalidate what they told you. The answer is in the assertion. === THREAD ATTRIBUTION (Resource Scope) === When observations contain sections: - MAINTAIN thread attribution where thread-specific context matters (e.g., ongoing tasks, thread-specific preferences) - CONSOLIDATE cross-thread facts that are stable/universal (e.g., user profile, general preferences) - PRESERVE thread attribution for recent or context-specific observations - When consolidating, you may merge observations from multiple threads if they represent the same universal fact Example input: Date: Dec 4, 2025 * \u{1F534} (14:30) User prefers TypeScript * \u{1F7E1} (14:35) Working on auth feature Date: Dec 4, 2025 * \u{1F534} (15:00) User prefers TypeScript * \u{1F7E1} (15:05) Debugging API endpoint Example output (consolidated): Date: Dec 4, 2025 * \u{1F534} (14:30) User prefers TypeScript * \u{1F7E1} (14:35) Working on auth feature * \u{1F7E1} (15:05) Debugging API endpoint === OUTPUT FORMAT === Your output MUST use XML tags to structure the response: Put all consolidated observations here using the date-grouped format with priority emojis (\u{1F534}, \u{1F7E1}, \u{1F7E2}). Group related observations with indentation. State the current task(s) explicitly: - Primary: What the agent is currently working on - Secondary: Other pending tasks (mark as "waiting for user" if appropriate) Hint for the agent's immediate next message. Examples: - "I've updated the navigation model. Let me walk you through the changes..." - "The assistant should wait for the user to respond before continuing." - Call the view tool on src/example.ts to continue debugging. User messages are extremely important. If the user asks a question or gives a new task, make it clear in that this is the priority. If the assistant needs to respond to the user, indicate in that it should pause for user reply before continuing other tasks.${instruction ? ` === CUSTOM INSTRUCTIONS === ${instruction}` : ""}`; } var MAX_COMPRESSION_LEVEL = 4; var COMPRESSION_GUIDANCE = { 0: "", 1: ` ## COMPRESSION REQUIRED Your previous reflection was the same size or larger than the original observations. Please re-process with slightly more compression: - Towards the beginning, condense more observations into higher-level reflections - Closer to the end, retain more fine details (recent context matters more) - Memory is getting long - use a more condensed style throughout - Combine related items more aggressively but do not lose important specific details of names, places, events, and people - Combine repeated similar tool calls (e.g. multiple file views, searches, or edits in the same area) into a single summary line describing what was explored/changed and the outcome - Preserve \u2705 completion markers \u2014 they are memory signals that tell the assistant what is already resolved and help prevent repeated work - Preserve the concrete resolved outcome captured by \u2705 markers so the assistant knows what exactly is done Aim for a 8/10 detail level. `, 2: ` ## AGGRESSIVE COMPRESSION REQUIRED Your previous reflection was still too large after compression guidance. Please re-process with much more aggressive compression: - Towards the beginning, heavily condense observations into high-level summaries - Closer to the end, retain fine details (recent context matters more) - Memory is getting very long - use a significantly more condensed style throughout - Combine related items aggressively but do not lose important specific details of names, places, events, and people - Combine repeated similar tool calls (e.g. multiple file views, searches, or edits in the same area) into a single summary line describing what was explored/changed and the outcome - If the same file or module is mentioned across many observations, merge into one entry covering the full arc - Preserve \u2705 completion markers \u2014 they are memory signals that tell the assistant what is already resolved and help prevent repeated work - Preserve the concrete resolved outcome captured by \u2705 markers so the assistant knows what exactly is done - Remove redundant information and merge overlapping observations Aim for a 6/10 detail level. `, 3: ` ## CRITICAL COMPRESSION REQUIRED Your previous reflections have failed to compress sufficiently after multiple attempts. Please re-process with maximum compression: - Summarize the oldest observations (first 50-70%) into brief high-level paragraphs \u2014 only key facts, decisions, and outcomes - For the most recent observations (last 30-50%), retain important details but still use a condensed style - Ruthlessly merge related observations \u2014 if 10 observations are about the same topic, combine into 1-2 lines - Combine all tool call sequences (file views, searches, edits, builds) into outcome-only summaries \u2014 drop individual steps entirely - Drop procedural details (tool calls, retries, intermediate steps) \u2014 keep only final outcomes - Drop observations that are no longer relevant or have been superseded by newer information - Preserve \u2705 completion markers \u2014 they are memory signals that tell the assistant what is already resolved and help prevent repeated work - Preserve the concrete resolved outcome captured by \u2705 markers so the assistant knows what exactly is done - Preserve: names, dates, decisions, errors, user preferences, and architectural choices Aim for a 4/10 detail level. `, 4: ` ## EXTREME COMPRESSION REQUIRED Multiple compression attempts have failed. The content may already be dense from a prior reflection. You MUST dramatically reduce the number of observations while keeping the standard observation format (date groups with bullet points and priority emojis): - Tool call observations are the biggest source of bloat. Collapse ALL tool call sequences into outcome-only observations \u2014 e.g. 10 observations about viewing/searching/editing files become 1 observation about what was actually learned or achieved (e.g. "Investigated auth module and found token validation was skipping expiry check") - Never preserve individual tool calls (viewed file X, searched for Y, ran build) \u2014 only preserve what was discovered or accomplished - Consolidate many related observations into single, more generic observations - Merge all same-day date groups into at most 2-3 date groups per day - For older content, each topic or task should be at most 1-2 observations capturing the key outcome - For recent content, retain more detail but still merge related items aggressively - If multiple observations describe incremental progress on the same task, keep only the final state - Preserve \u2705 completion markers and their outcomes but merge related completions into fewer lines - Preserve: user preferences, key decisions, architectural choices, and unresolved issues Aim for a 2/10 detail level. Fewer, more generic observations are better than many specific ones that exceed the budget. ` }; function buildReflectorPrompt(observations, manualPrompt, compressionLevel, skipContinuationHints) { const level = typeof compressionLevel === "number" ? compressionLevel : compressionLevel ? 1 : 0; const reflectionView = stripObservationGroups(observations); let prompt = `## OBSERVATIONS TO REFLECT ON ${reflectionView} --- Please analyze these observations and produce a refined, condensed version that will become the assistant's entire memory going forward.`; if (manualPrompt) { prompt += ` ## SPECIFIC GUIDANCE ${manualPrompt}`; } const guidance = COMPRESSION_GUIDANCE[level]; if (guidance) { prompt += ` ${guidance}`; } if (skipContinuationHints) { prompt += ` IMPORTANT: Do NOT include or sections in your output. Only output .`; } return prompt; } function parseReflectorOutput(output, sourceObservations) { if (detectDegenerateRepetition(output)) { return { observations: "", degenerate: true }; } const parsed = parseReflectorSectionXml(output); const sanitizedObservations = sanitizeObservationLines(stripEphemeralAnchorIds(parsed.observations || "")); const reconciledObservations = sourceObservations ? reconcileObservationGroupsFromReflection(sanitizedObservations, sourceObservations) : null; return { observations: reconciledObservations ?? sanitizedObservations, suggestedContinuation: parsed.suggestedResponse || void 0 // Note: Reflector's currentTask is not used - thread metadata preserves per-thread tasks }; } function parseReflectorSectionXml(content) { const result = { observations: "", currentTask: "", suggestedResponse: "" }; const observationsRegex = /^[ \t]*([\s\S]*?)^[ \t]*<\/observations>/gim; const observationsMatches = [...content.matchAll(observationsRegex)]; if (observationsMatches.length > 0) { result.observations = observationsMatches.map((m) => m[1]?.trim() ?? "").filter(Boolean).join("\n"); } else { const listItems = extractReflectorListItems(content); result.observations = listItems || content.trim(); } const currentTaskMatch = content.match(/([\s\S]*?)<\/current-task>/i); if (currentTaskMatch?.[1]) { result.currentTask = currentTaskMatch[1].trim(); } const suggestedResponseMatch = content.match(/([\s\S]*?)<\/suggested-response>/i); if (suggestedResponseMatch?.[1]) { result.suggestedResponse = suggestedResponseMatch[1].trim(); } return result; } function extractReflectorListItems(content) { const lines = content.split("\n"); const listLines = []; for (const line of lines) { if (/^\s*[-*]\s/.test(line) || /^\s*\d+\.\s/.test(line)) { listLines.push(line); } } return listLines.join("\n").trim(); } function validateCompression(reflectedTokens, targetThreshold) { return reflectedTokens < targetThreshold; } // src/processors/observational-memory/reflector-runner.ts function formatModelContext(provider, modelId) { if (provider && modelId) { return `${provider}/${modelId}`; } return modelId; } function getCurrentModel(model) { return formatModelContext(model?.provider, model?.modelId); } function getLastModelFromMessageList(messageList) { const messages = messageList?.get.all.db(); if (!messages) return void 0; for (let i = messages.length - 1; i >= 0; i--) { const message = messages[i]; if (!message || message.role !== "assistant" || !message.content || typeof message.content === "string") { continue; } for (let j = message.content.parts.length - 1; j >= 0; j--) { const part = message.content.parts[j]; if (part?.type === "step-start" && typeof part.model === "string" && part.model.length > 0) { return part.model; } } const metadata = message.content.metadata; const model = formatModelContext(metadata?.provider, metadata?.modelId); if (model) { return model; } } return void 0; } async function withAbortCheck(fn, abortSignal) { if (abortSignal?.aborted) throw new Error("The operation was aborted."); const result = await fn(); if (abortSignal?.aborted) throw new Error("The operation was aborted."); return result; } var EARLY_ACTIVATION_SIZE_FLOOR_RATIO = 0.75; var ReflectorRunner = class { reflectionConfig; observationConfig; tokenCounter; resolveModel; storage; scope; buffering; emitDebugEvent; persistMarkerToStorage; persistMarkerToMessage; getCompressionStartLevel; mastra; constructor(opts) { this.reflectionConfig = opts.reflectionConfig; this.observationConfig = opts.observationConfig; this.tokenCounter = opts.tokenCounter; this.resolveModel = opts.resolveModel; this.storage = opts.storage; this.scope = opts.scope; this.buffering = opts.buffering; this.emitDebugEvent = opts.emitDebugEvent; this.persistMarkerToStorage = opts.persistMarkerToStorage; this.persistMarkerToMessage = opts.persistMarkerToMessage; this.getCompressionStartLevel = opts.getCompressionStartLevel; this.mastra = opts.mastra; } __registerMastra(mastra) { this.mastra = mastra; } createAgent(model) { const agent$1 = new agent.Agent({ id: "observational-memory-reflector", name: "Reflector", instructions: buildReflectorSystemPrompt(this.reflectionConfig.instruction), model }); if (this.mastra) { agent$1.__registerMastra(this.mastra); } return agent$1; } getObservationMarkerConfig(record) { return { messageTokens: getMaxThreshold(this.observationConfig.messageTokens), observationTokens: getMaxThreshold( record ? this.getEffectiveReflectionTokens(record) : this.reflectionConfig.observationTokens ), scope: this.scope, activateAfterIdle: this.reflectionConfig.activateAfterIdle }; } /** * Resolve the effective reflection observationTokens for a record. * Only explicit per-record overrides (stored under `_overrides`) win; * the initial config snapshot is ignored so instance-level changes * still take effect for existing records. */ getEffectiveReflectionTokens(record) { const overrides = record.config?._overrides; const recordTokens = overrides?.reflection?.observationTokens; if (recordTokens) { return recordTokens; } return this.reflectionConfig.observationTokens; } /** * Call the Reflector agent with escalating compression levels. */ async call(observations, manualPrompt, streamContext, observationTokensThreshold, abortSignal, skipContinuationHints, compressionStartLevel, requestContext, observabilityContext, model) { const originalTokens = this.tokenCounter.countObservations(observations); const resolvedModel = model ? { model } : this.resolveModel(originalTokens); const agent = this.createAgent(resolvedModel.model); const internalRequestContext = withOmInternalThreadId(requestContext, agent.id); const targetThreshold = observationTokensThreshold ?? getMaxThreshold(this.reflectionConfig.observationTokens); let totalUsage = { inputTokens: 0, outputTokens: 0, totalTokens: 0 }; const startLevel = compressionStartLevel ?? 0; let currentLevel = startLevel; const maxLevel = Math.min(MAX_COMPRESSION_LEVEL, startLevel + 3); let parsed = { observations: "", suggestedContinuation: void 0 }; let reflectedTokens = 0; let attemptNumber = 0; while (currentLevel <= maxLevel) { attemptNumber++; const isRetry = attemptNumber > 1; const prompt = buildReflectorPrompt(observations, manualPrompt, currentLevel, skipContinuationHints); omDebug( `[OM:callReflector] ${isRetry ? `retry #${attemptNumber - 1}` : "first attempt"}: level=${currentLevel}, originalTokens=${originalTokens}, targetThreshold=${targetThreshold}, promptLen=${prompt.length}, skipContinuationHints=${skipContinuationHints}` ); let chunkCount = 0; const result = await withRetry( () => withOmTracingSpan({ phase: "reflector", model: resolvedModel.model, inputTokens: originalTokens, requestContext, observabilityContext, metadata: { omCompressionLevel: currentLevel, omCompressionAttempt: attemptNumber, omTargetThreshold: targetThreshold, omSkipContinuationHints: skipContinuationHints ?? false, ...resolvedModel.selectedThreshold !== void 0 ? { omSelectedThreshold: resolvedModel.selectedThreshold } : {}, ...resolvedModel.routingStrategy ? { omRoutingStrategy: resolvedModel.routingStrategy } : {}, ...resolvedModel.routingThresholds ? { omRoutingThresholds: resolvedModel.routingThresholds } : {} }, callback: (childObservabilityContext) => withAbortCheck(async () => { chunkCount = 0; const streamResult = await agent.stream(prompt, { modelSettings: { ...this.reflectionConfig.modelSettings }, providerOptions: this.reflectionConfig.providerOptions, ...abortSignal ? { abortSignal } : {}, ...internalRequestContext ? { requestContext: internalRequestContext } : {}, ...childObservabilityContext, ...attemptNumber === 1 ? { onChunk(chunk) { chunkCount++; if (chunkCount === 1 || chunkCount % 50 === 0) { const preview = chunk.type === "text-delta" ? ` text="${chunk.textDelta?.slice(0, 80)}..."` : chunk.type === "tool-call" ? ` tool=${chunk.toolName}` : ""; omDebug(`[OM:callReflector] chunk#${chunkCount}: type=${chunk.type}${preview}`); } }, onFinish(event) { omDebug( `[OM:callReflector] onFinish: chunks=${chunkCount}, finishReason=${event.finishReason}, inputTokens=${event.usage?.inputTokens}, outputTokens=${event.usage?.outputTokens}, textLen=${event.text?.length}` ); }, onAbort(event) { omDebug( `[OM:callReflector] onAbort: chunks=${chunkCount}, reason=${event?.reason ?? "unknown"}` ); }, onError({ error }) { omError(`[OM:callReflector] onError after ${chunkCount} chunks`, error); } } : {} }); return streamResult.getFullOutput(); }, abortSignal) }), { label: "reflector", abortSignal } ); omDebug( `[OM:callReflector] attempt #${attemptNumber} returned: textLen=${result.text?.length}, textPreview="${result.text?.slice(0, 120)}...", inputTokens=${result.usage?.inputTokens ?? result.totalUsage?.inputTokens}, outputTokens=${result.usage?.outputTokens ?? result.totalUsage?.outputTokens}` ); const usage = result.totalUsage ?? result.usage; if (usage) { totalUsage.inputTokens += usage.inputTokens ?? 0; totalUsage.outputTokens += usage.outputTokens ?? 0; totalUsage.totalTokens += usage.totalTokens ?? 0; } parsed = parseReflectorOutput(result.text, observations); if (parsed.degenerate) { omDebug( `[OM:callReflector] attempt #${attemptNumber}: degenerate repetition detected, treating as compression failure` ); reflectedTokens = originalTokens; } else { reflectedTokens = this.tokenCounter.countObservations(parsed.observations); } omDebug( `[OM:callReflector] attempt #${attemptNumber} parsed: reflectedTokens=${reflectedTokens}, targetThreshold=${targetThreshold}, compressionValid=${validateCompression(reflectedTokens, targetThreshold)}, parsedObsLen=${parsed.observations?.length}, degenerate=${parsed.degenerate ?? false}` ); if (!parsed.degenerate && (validateCompression(reflectedTokens, targetThreshold) || currentLevel >= maxLevel)) { break; } if (parsed.degenerate && currentLevel >= maxLevel) { omDebug(`[OM:callReflector] degenerate output persists at maxLevel=${maxLevel}, breaking`); break; } if (streamContext?.writer) { const failedMarker = createObservationFailedMarker({ cycleId: streamContext.cycleId, operationType: "reflection", startedAt: streamContext.startedAt, tokensAttempted: originalTokens, error: `Did not compress below threshold (${originalTokens} \u2192 ${reflectedTokens}, target: ${targetThreshold}), retrying at level ${currentLevel + 1}`, recordId: streamContext.recordId, threadId: streamContext.threadId }); await streamContext.writer.custom({ ...failedMarker, transient: true }).catch(() => { }); await this.persistMarkerToStorage(failedMarker, streamContext.threadId, streamContext.resourceId); const retryCycleId = crypto.randomUUID(); streamContext.cycleId = retryCycleId; const startMarker = createObservationStartMarker({ cycleId: retryCycleId, operationType: "reflection", tokensToObserve: originalTokens, recordId: streamContext.recordId, threadId: streamContext.threadId, threadIds: [streamContext.threadId], config: this.getObservationMarkerConfig() }); streamContext.startedAt = startMarker.data.startedAt; await streamContext.writer.custom({ ...startMarker, transient: true }).catch(() => { }); await this.persistMarkerToStorage(startMarker, streamContext.threadId, streamContext.resourceId); } currentLevel = Math.min(currentLevel + 1, maxLevel); } return { observations: parsed.observations, suggestedContinuation: parsed.suggestedContinuation, usage: totalUsage.totalTokens > 0 ? totalUsage : void 0 }; } /** * Start an async buffered reflection in the background. */ startAsyncBufferedReflection(record, observationTokens, lockKey, writer, requestContext, observabilityContext, reflectionHooks) { const bufferKey = this.buffering.getReflectionBufferKey(lockKey); if (this.buffering.isAsyncBufferingInProgress(bufferKey)) { return; } BufferingCoordinator.lastBufferedBoundary.set(bufferKey, observationTokens); registerOp(record.id, "bufferingReflection"); this.storage.setBufferingReflectionFlag(record.id, true).catch((err) => { omError("[OM] Failed to set buffering reflection flag", err); }); reflectionHooks?.onReflectionStart?.(); const asyncOp = this.doAsyncBufferedReflection(record, bufferKey, writer, requestContext, observabilityContext).then((usage) => { reflectionHooks?.onReflectionEnd?.({ usage }); }).catch(async (error) => { if (writer) { const failedMarker = createBufferingFailedMarker({ cycleId: `reflect-buf-${Date.now()}-${Math.random().toString(36).slice(2, 11)}`, operationType: "reflection", startedAt: (/* @__PURE__ */ new Date()).toISOString(), tokensAttempted: observationTokens, error: error instanceof Error ? error.message : String(error), recordId: record.id, threadId: record.threadId ?? "" }); void writer.custom({ ...failedMarker, transient: true }).catch(() => { }); await this.persistMarkerToStorage(failedMarker, record.threadId ?? "", record.resourceId ?? void 0); } omError("[OM] Async buffered reflection failed", error); reflectionHooks?.onReflectionEnd?.({ usage: void 0, error: error instanceof Error ? error : new Error(String(error)) }); BufferingCoordinator.lastBufferedBoundary.delete(bufferKey); }).finally(() => { BufferingCoordinator.asyncBufferingOps.delete(bufferKey); unregisterOp(record.id, "bufferingReflection"); this.storage.setBufferingReflectionFlag(record.id, false).catch((err) => { omError("[OM] Failed to clear buffering reflection flag", err); }); }); BufferingCoordinator.asyncBufferingOps.set(bufferKey, asyncOp); } /** * Perform async buffered reflection — reflects observations and stores to bufferedReflection. * Does NOT create a new generation or update activeObservations. */ async doAsyncBufferedReflection(record, _bufferKey, writer, requestContext, observabilityContext) { const freshRecord = await this.storage.getObservationalMemory(record.threadId, record.resourceId); const currentRecord = freshRecord ?? record; const observationTokens = currentRecord.observationTokenCount ?? 0; const reflectThreshold = getMaxThreshold(this.getEffectiveReflectionTokens(currentRecord)); const bufferActivation = this.reflectionConfig.bufferActivation ?? 0.5; const startedAt = (/* @__PURE__ */ new Date()).toISOString(); const cycleId = `reflect-buf-${Date.now()}-${Math.random().toString(36).slice(2, 11)}`; BufferingCoordinator.reflectionBufferCycleIds.set(_bufferKey, cycleId); const fullObservations = currentRecord.activeObservations ?? ""; const allLines = fullObservations.split("\n"); const totalLines = allLines.length; const avgTokensPerLine = totalLines > 0 ? observationTokens / totalLines : 0; const activationPointTokens = reflectThreshold * bufferActivation; const linesToReflect = avgTokensPerLine > 0 ? Math.min(Math.floor(activationPointTokens / avgTokensPerLine), totalLines) : totalLines; const activeObservations = allLines.slice(0, linesToReflect).join("\n"); const reflectedObservationLineCount = linesToReflect; const sliceTokenEstimate = Math.round(avgTokensPerLine * linesToReflect); const compressionTarget = Math.round(sliceTokenEstimate * 0.75); omDebug( `[OM:reflect] doAsyncBufferedReflection: slicing observations for reflection \u2014 totalLines=${totalLines}, avgTokPerLine=${avgTokensPerLine.toFixed(1)}, activationPointTokens=${activationPointTokens}, linesToReflect=${linesToReflect}/${totalLines}, sliceTokenEstimate=${sliceTokenEstimate}, compressionTarget=${compressionTarget}` ); omDebug( `[OM:reflect] doAsyncBufferedReflection: starting reflector call, recordId=${currentRecord.id}, observationTokens=${sliceTokenEstimate}, compressionTarget=${compressionTarget} (inputTokens), activeObsLength=${activeObservations.length}, reflectedLineCount=${reflectedObservationLineCount}` ); if (writer) { const startMarker = createBufferingStartMarker({ cycleId, operationType: "reflection", tokensToBuffer: sliceTokenEstimate, recordId: record.id, threadId: record.threadId ?? "", threadIds: record.threadId ? [record.threadId] : [], config: this.getObservationMarkerConfig(currentRecord) }); void writer.custom({ ...startMarker, transient: true }).catch(() => { }); await this.persistMarkerToStorage( startMarker, currentRecord.threadId ?? "", currentRecord.resourceId ?? void 0 ); } const compressionStartLevel = await this.getCompressionStartLevel(requestContext); const reflectResult = await this.call( activeObservations, void 0, void 0, compressionTarget, void 0, true, compressionStartLevel, requestContext, observabilityContext ); const reflectionTokenCount = this.tokenCounter.countObservations(reflectResult.observations); omDebug( `[OM:reflect] doAsyncBufferedReflection: reflector returned ${reflectionTokenCount} tokens (${reflectResult.observations?.length} chars), saving to recordId=${currentRecord.id}` ); await this.storage.updateBufferedReflection({ id: currentRecord.id, reflection: reflectResult.observations, tokenCount: reflectionTokenCount, inputTokenCount: sliceTokenEstimate, reflectedObservationLineCount }); omDebug( `[OM:reflect] doAsyncBufferedReflection: bufferedReflection saved with lineCount=${reflectedObservationLineCount}` ); if (writer) { const endMarker = createBufferingEndMarker({ cycleId, operationType: "reflection", startedAt, tokensBuffered: sliceTokenEstimate, bufferedTokens: reflectionTokenCount, recordId: currentRecord.id, threadId: currentRecord.threadId ?? "", observations: reflectResult.observations }); void writer.custom({ ...endMarker, transient: true }).catch(() => { }); await this.persistMarkerToStorage(endMarker, currentRecord.threadId ?? "", currentRecord.resourceId ?? void 0); } return reflectResult.usage; } /** * Try to activate buffered reflection when threshold is reached. * Returns a discriminated result so the caller can distinguish between * "activated", "no buffer present", and "suppressed by overshoot guard" * without re-deriving that state. */ async tryActivateBufferedReflection(record, lockKey, writer, messageList, activationMetadata) { const bufferKey = this.buffering.getReflectionBufferKey(lockKey); const asyncOp = BufferingCoordinator.asyncBufferingOps.get(bufferKey); if (asyncOp) { if (activationMetadata?.triggeredBy === "ttl" || activationMetadata?.triggeredBy === "provider_change") { omDebug( `[OM:reflect] tryActivateBufferedReflection: async op in progress, not blocking for ${activationMetadata.triggeredBy} trigger` ); } else { omDebug(`[OM:reflect] tryActivateBufferedReflection: waiting for in-progress op...`); try { await Promise.race([ asyncOp, new Promise((_, reject) => setTimeout(() => reject(new Error("Timeout")), 5e3)) ]); } catch { } } } const freshRecord = await this.storage.getObservationalMemory(record.threadId, record.resourceId); omDebug( `[OM:reflect] tryActivateBufferedReflection: recordId=${record.id}, hasBufferedReflection=${!!freshRecord?.bufferedReflection}, bufferedReflectionLen=${freshRecord?.bufferedReflection?.length ?? 0}` ); omDebug( `[OM:reflect] tryActivateBufferedReflection: freshRecord.id=${freshRecord?.id}, freshBufferedReflection=${freshRecord?.bufferedReflection ? "present (" + freshRecord.bufferedReflection.length + " chars)" : "empty"}, freshObsTokens=${freshRecord?.observationTokenCount}` ); if (!freshRecord?.bufferedReflection) { omDebug(`[OM:reflect] tryActivateBufferedReflection: no buffered reflection after re-fetch`); return { status: "no-buffer" }; } const beforeTokens = freshRecord.observationTokenCount ?? 0; const reflectedLineCount = freshRecord.reflectedObservationLineCount ?? 0; const currentObservations = freshRecord.activeObservations ?? ""; const allLines = currentObservations.split("\n"); const unreflectedLines = allLines.slice(reflectedLineCount); const unreflectedContent = unreflectedLines.join("\n").trim(); const combinedObservations = unreflectedContent ? `${freshRecord.bufferedReflection} ${unreflectedContent}` : freshRecord.bufferedReflection; const combinedTokenCount = this.tokenCounter.countObservations(combinedObservations); if (activationMetadata?.triggeredBy === "ttl" || activationMetadata?.triggeredBy === "provider_change") { const unreflectedTailTokens = unreflectedContent ? this.tokenCounter.countObservations(unreflectedContent) : 0; const bufferedReflectionTokens = freshRecord.bufferedReflectionTokens ?? 0; if (unreflectedTailTokens < bufferedReflectionTokens) { omDebug( `[OM:reflect] tryActivateBufferedReflection: suppressing early ${activationMetadata.triggeredBy} activation \u2014 unreflectedTailTokens=${unreflectedTailTokens} < bufferedReflectionTokens=${bufferedReflectionTokens}; keeping buffer for threshold activation` ); return { status: "suppressed", reason: "composition" }; } const bufferActivation = this.reflectionConfig.bufferActivation; const reflectThreshold = getMaxThreshold(this.getEffectiveReflectionTokens(freshRecord)); const regularActivationTarget = reflectThreshold * (1 - bufferActivation); const minCombinedTokens = Math.round(regularActivationTarget * EARLY_ACTIVATION_SIZE_FLOOR_RATIO); if (combinedTokenCount < minCombinedTokens) { omDebug( `[OM:reflect] tryActivateBufferedReflection: suppressing early ${activationMetadata.triggeredBy} activation \u2014 combinedTokenCount=${combinedTokenCount} < minCombinedTokens=${minCombinedTokens} (${EARLY_ACTIVATION_SIZE_FLOOR_RATIO * 100}% of regular activation target ${Math.round(regularActivationTarget)}, threshold=${reflectThreshold}, bufferActivation=${bufferActivation}); keeping buffer for threshold activation` ); return { status: "suppressed", reason: "size" }; } } omDebug( `[OM:reflect] tryActivateBufferedReflection: activating, beforeTokens=${beforeTokens}, combinedTokenCount=${combinedTokenCount}, reflectedLineCount=${reflectedLineCount}, unreflectedLines=${unreflectedLines.length}` ); await this.storage.swapBufferedReflectionToActive({ currentRecord: freshRecord, tokenCount: combinedTokenCount }); BufferingCoordinator.lastBufferedBoundary.delete(bufferKey); const afterRecord = await this.storage.getObservationalMemory(record.threadId, record.resourceId); const afterTokens = afterRecord?.observationTokenCount ?? 0; omDebug( `[OM:reflect] tryActivateBufferedReflection: activation complete! beforeTokens=${beforeTokens}, afterTokens=${afterTokens}, newRecordId=${afterRecord?.id}, newGenCount=${afterRecord?.generationCount}` ); if (writer) { const originalCycleId = BufferingCoordinator.reflectionBufferCycleIds.get(bufferKey); const activationMarker = createActivationMarker({ cycleId: originalCycleId ?? `reflect-act-${Date.now()}-${Math.random().toString(36).slice(2, 11)}`, operationType: "reflection", chunksActivated: 1, tokensActivated: beforeTokens, observationTokens: afterTokens, messagesActivated: 0, recordId: freshRecord.id, threadId: freshRecord.threadId ?? "", generationCount: afterRecord?.generationCount ?? freshRecord.generationCount ?? 0, observations: afterRecord?.activeObservations, triggeredBy: activationMetadata?.triggeredBy, lastActivityAt: activationMetadata?.lastActivityAt, ttlExpiredMs: activationMetadata?.ttlExpiredMs, previousModel: activationMetadata?.previousModel, currentModel: activationMetadata?.currentModel, config: { ...this.getObservationMarkerConfig(freshRecord), activateAfterIdle: activationMetadata?.activateAfterIdle ?? this.reflectionConfig.activateAfterIdle } }); void writer.custom({ ...activationMarker, transient: true }).catch(() => { }); await this.persistMarkerToMessage( activationMarker, messageList, freshRecord.threadId ?? "", freshRecord.resourceId ?? void 0 ); } BufferingCoordinator.reflectionBufferCycleIds.delete(bufferKey); return { status: "activated" }; } /** * Check if reflection needed and trigger if so. * Supports both synchronous reflection and async buffered reflection. * @internal Used by observation strategies. Do not call directly. */ async maybeReflect(opts) { const { record, observationTokens, writer, abortSignal, messageList, currentModel, reflectionHooks, requestContext, observabilityContext, lastActivityAt, threadId: requestedThreadId } = opts; const lockKey = this.buffering.getLockKey(record.threadId, record.resourceId); const reflectThreshold = getMaxThreshold(this.getEffectiveReflectionTokens(record)); if (this.buffering.isAsyncReflectionEnabled() && observationTokens < reflectThreshold) { const shouldTrigger = (() => { if (!this.buffering.isAsyncReflectionEnabled()) return false; if (record.isBufferingReflection) { if (isOpActiveInProcess(record.id, "bufferingReflection")) return false; omDebug(`[OM:shouldTriggerAsyncRefl] isBufferingReflection=true but stale, clearing`); this.storage.setBufferingReflectionFlag(record.id, false).catch(() => { }); } const bufferKey = this.buffering.getReflectionBufferKey(lockKey); if (this.buffering.isAsyncBufferingInProgress(bufferKey)) return false; if (BufferingCoordinator.lastBufferedBoundary.has(bufferKey)) return false; if (record.bufferedReflection) return false; const activationPoint = reflectThreshold * this.reflectionConfig.bufferActivation; return observationTokens >= activationPoint; })(); if (shouldTrigger) { this.startAsyncBufferedReflection( record, observationTokens, lockKey, writer, requestContext, observabilityContext, reflectionHooks ); } } const activateAfterIdle = resolveActivationTTL(this.reflectionConfig.activateAfterIdle, currentModel); const ttlExpiredMs = activateAfterIdle !== void 0 && lastActivityAt !== void 0 ? Date.now() - lastActivityAt : void 0; const ttlExpired = ttlExpiredMs !== void 0 && activateAfterIdle !== void 0 && ttlExpiredMs >= activateAfterIdle; const actorModel = getCurrentModel(currentModel); const lastModel = getLastModelFromMessageList(messageList); const providerChanged = this.reflectionConfig.activateOnProviderChange === true && didProviderChange(actorModel, lastModel); if (observationTokens < reflectThreshold && !ttlExpired && !providerChanged) { return; } const activationTriggeredBy = observationTokens >= reflectThreshold ? "threshold" : providerChanged ? "provider_change" : "ttl"; const activationMetadata = { triggeredBy: activationTriggeredBy, lastActivityAt: activationTriggeredBy === "ttl" ? lastActivityAt : void 0, activateAfterIdle: activationTriggeredBy === "ttl" ? activateAfterIdle : void 0, ttlExpiredMs: activationTriggeredBy === "ttl" ? ttlExpiredMs : void 0, previousModel: activationTriggeredBy === "provider_change" ? lastModel : void 0, currentModel: activationTriggeredBy === "provider_change" ? actorModel : void 0 }; if (record.isReflecting) { if (isOpActiveInProcess(record.id, "reflecting")) { omDebug(`[OM:reflect] isReflecting=true and active in this process, skipping`); return; } omDebug(`[OM:reflect] isReflecting=true but NOT active in this process \u2014 stale flag from dead process, clearing`); await this.storage.setReflectingFlag(record.id, false); } if (this.buffering.isAsyncReflectionEnabled()) { const activationResult = await this.tryActivateBufferedReflection( record, lockKey, writer, messageList, activationMetadata ); if (activationResult.status === "activated") { return; } if (activationResult.status === "suppressed") { omDebug( `[OM:reflect] skipping sync fallback / re-buffer after suppressed early ${activationMetadata.triggeredBy} activation (reason=${activationResult.reason})` ); return; } if (this.reflectionConfig.blockAfter && observationTokens >= this.reflectionConfig.blockAfter) { omDebug( `[OM:reflect] blockAfter exceeded (${observationTokens} >= ${this.reflectionConfig.blockAfter}), falling through to sync reflection` ); } else { const activationPoint = reflectThreshold * this.reflectionConfig.bufferActivation; if (observationTokens < activationPoint) { omDebug( `[OM:reflect] skipping async reflection \u2014 observationTokens (${observationTokens}) below activation point (${activationPoint}), triggered by ${activationTriggeredBy}` ); return; } omDebug( `[OM:reflect] async activation failed, no blockAfter or below it (obsTokens=${observationTokens}, blockAfter=${this.reflectionConfig.blockAfter}) \u2014 starting background reflection` ); this.startAsyncBufferedReflection( record, observationTokens, lockKey, writer, requestContext, observabilityContext, reflectionHooks ); return; } } reflectionHooks?.onReflectionStart?.(); await this.storage.setReflectingFlag(record.id, true); registerOp(record.id, "reflecting"); const cycleId = crypto.randomUUID(); const startedAt = (/* @__PURE__ */ new Date()).toISOString(); const threadId = requestedThreadId ?? "unknown"; if (writer) { const startMarker = createObservationStartMarker({ cycleId, operationType: "reflection", tokensToObserve: observationTokens, recordId: record.id, threadId, threadIds: [threadId], config: this.getObservationMarkerConfig(record) }); await writer.custom({ ...startMarker, transient: true }).catch(() => { }); await this.persistMarkerToStorage(startMarker, threadId, record.resourceId ?? void 0); } this.emitDebugEvent({ type: "reflection_triggered", timestamp: /* @__PURE__ */ new Date(), threadId, resourceId: record.resourceId ?? "", inputTokens: observationTokens, activeObservationsLength: record.activeObservations?.length ?? 0 }); const streamContext = writer ? { writer, cycleId, startedAt, recordId: record.id, threadId, resourceId: record.resourceId ?? void 0 } : void 0; let reflectionUsage; let reflectionError; try { const compressionStartLevel = await this.getCompressionStartLevel(requestContext); const reflectResult = await this.call( record.activeObservations, void 0, streamContext, reflectThreshold, abortSignal, void 0, compressionStartLevel, requestContext, observabilityContext ); reflectionUsage = reflectResult.usage; const reflectionTokenCount = this.tokenCounter.countObservations(reflectResult.observations); await this.storage.createReflectionGeneration({ currentRecord: record, reflection: reflectResult.observations, tokenCount: reflectionTokenCount }); if (writer && streamContext) { const endMarker = createObservationEndMarker({ cycleId: streamContext.cycleId, operationType: "reflection", startedAt: streamContext.startedAt, tokensObserved: observationTokens, observationTokens: reflectionTokenCount, observations: reflectResult.observations, recordId: record.id, threadId }); await writer.custom({ ...endMarker, transient: true }).catch(() => { }); await this.persistMarkerToStorage(endMarker, threadId, record.resourceId ?? void 0); } this.emitDebugEvent({ type: "reflection_complete", timestamp: /* @__PURE__ */ new Date(), threadId, resourceId: record.resourceId ?? "", inputTokens: observationTokens, outputTokens: reflectionTokenCount, observations: reflectResult.observations, usage: reflectResult.usage }); } catch (error) { if (writer && streamContext) { const failedMarker = createObservationFailedMarker({ cycleId: streamContext.cycleId, operationType: "reflection", startedAt: streamContext.startedAt, tokensAttempted: observationTokens, error: error instanceof Error ? error.message : String(error), recordId: record.id, threadId }); await writer.custom({ ...failedMarker, transient: true }).catch(() => { }); await this.persistMarkerToStorage(failedMarker, threadId, record.resourceId ?? void 0); } reflectionError = error instanceof Error ? error : new Error(String(error)); if (abortSignal?.aborted) { throw error; } omError("[OM] Reflection failed", error); } finally { await this.storage.setReflectingFlag(record.id, false); reflectionHooks?.onReflectionEnd?.({ usage: reflectionUsage, error: reflectionError }); unregisterOp(record.id, "reflecting"); } } }; function getOmReproCaptureDir() { return process.env.OM_REPRO_CAPTURE_DIR ?? ".mastra-om-repro"; } function sanitizeCapturePathSegment(value) { const sanitized = value.replace(/[\\/]+/g, "_").replace(/\.{2,}/g, "_").trim(); return sanitized.length > 0 ? sanitized : "unknown-thread"; } function isOmReproCaptureEnabled() { return process.env.OM_REPRO_CAPTURE === "1"; } function safeCaptureJson(value) { return JSON.parse( JSON.stringify(value, (_key, current) => { if (typeof current === "bigint") return current.toString(); if (typeof current === "function") return "[function]"; if (typeof current === "symbol") return current.toString(); if (current instanceof Error) return { name: current.name, message: current.message, stack: current.stack }; if (current instanceof Set) return { __type: "Set", values: Array.from(current.values()) }; if (current instanceof Map) return { __type: "Map", entries: Array.from(current.entries()) }; return current; }) ); } function safeCaptureJsonOrError(value) { try { return { ok: true, value: safeCaptureJson(value) }; } catch (error) { return { ok: false, error: safeCaptureJson({ message: error instanceof Error ? error.message : String(error), stack: error instanceof Error ? error.stack : void 0, inspected: util.inspect(value, { depth: 3, maxArrayLength: 20, breakLength: 120 }) }) }; } } function formatCaptureDate(value) { if (!value) return void 0; if (value instanceof Date) return value.toISOString(); try { return new Date(value).toISOString(); } catch { return void 0; } } function summarizeOmTurn(value) { if (!value || typeof value !== "object") { return value; } const turn = value; return { __type: "ObservationTurn", threadId: turn.threadId, resourceId: turn.resourceId, started: turn._started, ended: turn._ended, generationCountAtStart: turn._generationCountAtStart, record: turn._record ? { id: turn._record.id, scope: turn._record.scope, threadId: turn._record.threadId, resourceId: turn._record.resourceId, createdAt: formatCaptureDate(turn._record.createdAt), updatedAt: formatCaptureDate(turn._record.updatedAt), lastObservedAt: formatCaptureDate(turn._record.lastObservedAt), generationCount: turn._record.generationCount, observationTokenCount: turn._record.observationTokenCount, pendingMessageTokens: turn._record.pendingMessageTokens, isBufferingObservation: turn._record.isBufferingObservation, isBufferingReflection: turn._record.isBufferingReflection } : void 0, context: turn._context ? { messageCount: Array.isArray(turn._context.messages) ? turn._context.messages.length : void 0, hasSystemMessage: Array.isArray(turn._context.systemMessage) ? turn._context.systemMessage.length > 0 : Boolean(turn._context.systemMessage), continuationId: turn._context.continuation?.id, hasOtherThreadsContext: Boolean(turn._context.otherThreadsContext), recordId: turn._context.record?.id } : void 0, currentStep: turn._currentStep ? { stepNumber: turn._currentStep.stepNumber, prepared: turn._currentStep._prepared, context: turn._currentStep._context ? { activated: turn._currentStep._context.activated, observed: turn._currentStep._context.observed, buffered: turn._currentStep._context.buffered, reflected: turn._currentStep._context.reflected, didThresholdCleanup: turn._currentStep._context.didThresholdCleanup, messageCount: Array.isArray(turn._currentStep._context.messages) ? turn._currentStep._context.messages.length : void 0, systemMessageCount: Array.isArray(turn._currentStep._context.systemMessage) ? turn._currentStep._context.systemMessage.length : void 0 } : void 0 } : void 0 }; } function sanitizeCaptureState(rawState) { return Object.fromEntries( Object.entries(rawState).map(([key, value]) => { if (key === "__omTurn") { return [key, summarizeOmTurn(value)]; } return [key, value]; }) ); } function buildReproMessageFingerprint(message) { const createdAt = message.createdAt instanceof Date ? message.createdAt.toISOString() : message.createdAt ? new Date(message.createdAt).toISOString() : ""; return JSON.stringify({ role: message.role, createdAt, content: message.content }); } function inferReproIdRemap(preMessages, postMessages) { const preByFingerprint = /* @__PURE__ */ new Map(); const postByFingerprint = /* @__PURE__ */ new Map(); for (const message of preMessages) { if (!message.id) continue; const fingerprint = buildReproMessageFingerprint(message); const list = preByFingerprint.get(fingerprint) ?? []; list.push(message.id); preByFingerprint.set(fingerprint, list); } for (const message of postMessages) { if (!message.id) continue; const fingerprint = buildReproMessageFingerprint(message); const list = postByFingerprint.get(fingerprint) ?? []; list.push(message.id); postByFingerprint.set(fingerprint, list); } const remap = []; for (const [fingerprint, preIds] of preByFingerprint.entries()) { const postIds = postByFingerprint.get(fingerprint); if (!postIds || preIds.length !== 1 || postIds.length !== 1) continue; const fromId = preIds[0]; const toId = postIds[0]; if (!fromId || !toId || fromId === toId) { continue; } remap.push({ fromId, toId, fingerprint }); } return remap; } function createOmReproCaptureDir(threadId, label) { const sanitizedThreadId = sanitizeCapturePathSegment(threadId); const captureDir = path.join( process.cwd(), getOmReproCaptureDir(), sanitizedThreadId, `${Date.now()}-${label}-${crypto$1.randomUUID()}` ); fs.mkdirSync(captureDir, { recursive: true }); return captureDir; } function writeObserverExchangeReproCapture(params) { if (!isOmReproCaptureEnabled() || !params.observerExchange) { return; } try { const captureDir = createOmReproCaptureDir(params.threadId, params.label); const payloads = [ { fileName: "input.json", data: { threadId: params.threadId, resourceId: params.resourceId, label: params.label } }, { fileName: "output.json", data: { details: params.details ?? {} } }, { fileName: "observer-exchange.json", data: params.observerExchange } ]; const captureErrors = []; for (const payload of payloads) { const serialized = safeCaptureJsonOrError(payload.data); if (serialized.ok) { fs.writeFileSync(path.join(captureDir, payload.fileName), `${JSON.stringify(serialized.value, null, 2)} `); continue; } captureErrors.push({ fileName: payload.fileName, error: serialized.error }); fs.writeFileSync( path.join(captureDir, payload.fileName), `${JSON.stringify({ __captureError: serialized.error }, null, 2)} ` ); } if (captureErrors.length > 0) { fs.writeFileSync(path.join(captureDir, "capture-error.json"), `${JSON.stringify(captureErrors, null, 2)} `); params.debug?.( `[OM:repro-capture] wrote ${params.label} capture with ${captureErrors.length} serialization error(s) to ${captureDir}` ); return; } params.debug?.(`[OM:repro-capture] wrote ${params.label} capture to ${captureDir}`); } catch (error) { params.debug?.(`[OM:repro-capture] failed to write ${params.label} capture: ${String(error)}`); } } function writeProcessInputStepReproCapture(params) { if (!isOmReproCaptureEnabled()) { return; } try { const captureDir = createOmReproCaptureDir(params.threadId, `step-${params.stepNumber}`); const contextMessages = params.messageList.get.all.db(); const memoryContext = memory.parseMemoryRequestContext(params.args.requestContext); const preMessageIds = new Set(params.preMessages.map((message) => message.id)); const postMessageIds = new Set(contextMessages.map((message) => message.id)); const removedMessageIds = params.preMessages.map((message) => message.id).filter((id) => Boolean(id) && !postMessageIds.has(id)); const addedMessageIds = contextMessages.map((message) => message.id).filter((id) => Boolean(id) && !preMessageIds.has(id)); const idRemap = inferReproIdRemap(params.preMessages, contextMessages); const rawState = params.args.state ?? {}; const sanitizedState = sanitizeCaptureState(rawState); const payloads = [ { fileName: "input.json", data: { stepNumber: params.stepNumber, threadId: params.threadId, resourceId: params.resourceId, readOnly: memoryContext?.memoryConfig?.readOnly, messageCount: contextMessages.length, messageIds: contextMessages.map((message) => message.id), stateKeys: Object.keys(rawState), state: sanitizedState, args: { messages: params.args.messages, steps: params.args.steps, systemMessages: params.args.systemMessages, retryCount: params.args.retryCount, toolChoice: params.args.toolChoice, activeTools: params.args.activeTools, modelSettings: params.args.modelSettings, structuredOutput: params.args.structuredOutput } } }, { fileName: "pre-state.json", data: { record: params.preRecord, bufferedChunks: params.preBufferedChunks, contextTokenCount: params.preContextTokenCount, messages: params.preMessages, messageList: params.preSerializedMessageList } }, { fileName: "output.json", data: { details: params.details, messageDiff: { removedMessageIds, addedMessageIds, idRemap } } }, { fileName: "post-state.json", data: { record: params.postRecord, bufferedChunks: params.postBufferedChunks, contextTokenCount: params.postContextTokenCount, messageCount: contextMessages.length, messageIds: contextMessages.map((message) => message.id), messages: contextMessages, messageList: params.messageList.serialize() } } ]; const captureErrors = []; for (const payload of payloads) { const serialized = safeCaptureJsonOrError(payload.data); if (serialized.ok) { fs.writeFileSync(path.join(captureDir, payload.fileName), `${JSON.stringify(serialized.value, null, 2)} `); continue; } captureErrors.push({ fileName: payload.fileName, error: serialized.error }); fs.writeFileSync( path.join(captureDir, payload.fileName), `${JSON.stringify({ __captureError: serialized.error }, null, 2)} ` ); } if (params.observerExchange) { const serialized = safeCaptureJsonOrError(params.observerExchange); if (serialized.ok) { fs.writeFileSync(path.join(captureDir, "observer-exchange.json"), `${JSON.stringify(serialized.value, null, 2)} `); } else { captureErrors.push({ fileName: "observer-exchange.json", error: serialized.error }); fs.writeFileSync( path.join(captureDir, "observer-exchange.json"), `${JSON.stringify({ __captureError: serialized.error }, null, 2)} ` ); } } if (captureErrors.length > 0) { fs.writeFileSync(path.join(captureDir, "capture-error.json"), `${JSON.stringify(captureErrors, null, 2)} `); params.debug?.( `[OM:repro-capture] wrote processInputStep capture with ${captureErrors.length} serialization error(s) to ${captureDir}` ); return; } params.debug?.(`[OM:repro-capture] wrote processInputStep capture to ${captureDir}`); } catch (error) { params.debug?.(`[OM:repro-capture] failed to write processInputStep capture: ${String(error)}`); } } var IMAGE_FILE_EXTENSIONS = /* @__PURE__ */ new Set([ "png", "jpg", "jpeg", "webp", "gif", "bmp", "tiff", "tif", "heic", "heif", "avif" ]); var TOKEN_ESTIMATE_CACHE_VERSION = 7; var CLIENT_TOKEN_ESTIMATE_SOURCE = "client"; var DEFAULT_IMAGE_ESTIMATOR = { baseTokens: 85, tileTokens: 170, fallbackTiles: 4 }; var GOOGLE_LEGACY_IMAGE_TOKENS_PER_TILE = 258; var GOOGLE_GEMINI_3_IMAGE_TOKENS_BY_RESOLUTION = { low: 280, medium: 560, high: 1120, ultra_high: 2240, unspecified: 1120 }; var ANTHROPIC_IMAGE_TOKENS_PER_PIXEL = 1 / 750; var ANTHROPIC_IMAGE_MAX_LONG_EDGE = 1568; var GOOGLE_MEDIA_RESOLUTION_VALUES = /* @__PURE__ */ new Set([ "low", "medium", "high", "ultra_high", "unspecified" ]); var ATTACHMENT_COUNT_TIMEOUT_MS = 2e4; var REMOTE_IMAGE_PROBE_TIMEOUT_MS = 2500; var PROVIDER_API_KEY_ENV_VARS = { openai: ["OPENAI_API_KEY"], google: ["GOOGLE_GENERATIVE_AI_API_KEY", "GOOGLE_API_KEY"], anthropic: ["ANTHROPIC_API_KEY"] }; function getPartMastraMetadata(part) { return part.providerMetadata?.mastra; } function ensurePartMastraMetadata(part) { const typedPart = part; typedPart.providerMetadata ??= {}; typedPart.providerMetadata.mastra ??= {}; return typedPart.providerMetadata.mastra; } function getContentMastraMetadata(content) { if (!content || typeof content !== "object") { return void 0; } return content.metadata?.mastra; } function ensureContentMastraMetadata(content) { if (!content || typeof content !== "object") { return void 0; } const typedContent = content; typedContent.metadata ??= {}; typedContent.metadata.mastra ??= {}; return typedContent.metadata.mastra; } function getMessageMastraMetadata(message) { return message.metadata?.mastra; } function ensureMessageMastraMetadata(message) { const typedMessage = message; typedMessage.metadata ??= {}; typedMessage.metadata.mastra ??= {}; return typedMessage.metadata.mastra; } function buildEstimateKey(kind, text) { const payloadHash = crypto$1.createHash("sha256").update(text).digest("hex"); return `${kind}:${payloadHash}`; } function resolveEstimatorId() { return "tokenx"; } function isTokenEstimateEntry(value) { if (!value || typeof value !== "object") return false; const entry = value; return typeof entry.v === "number" && typeof entry.source === "string" && typeof entry.key === "string" && typeof entry.tokens === "number"; } function getCacheEntry(cache, key) { if (!cache || typeof cache !== "object") return void 0; if (isTokenEstimateEntry(cache)) { return cache.key === key ? cache : void 0; } const keyedEntry = cache[key]; return isTokenEstimateEntry(keyedEntry) ? keyedEntry : void 0; } function mergeCacheEntry(cache, key, entry) { if (isTokenEstimateEntry(cache)) { if (cache.key === key) { return entry; } return { [cache.key]: cache, [key]: entry }; } if (cache && typeof cache === "object") { return { ...cache, [key]: entry }; } return entry; } function getPartCacheEntry(part, key) { return getCacheEntry(getPartMastraMetadata(part)?.tokenEstimate, key); } function setPartCacheEntry(part, key, entry) { const mastraMetadata = ensurePartMastraMetadata(part); mastraMetadata.tokenEstimate = mergeCacheEntry(mastraMetadata.tokenEstimate, key, entry); } function getClientPartTokenEstimate(part) { const cache = getPartMastraMetadata(part)?.tokenEstimate; if (!cache || typeof cache !== "object") return void 0; const matches = (entry) => isTokenEstimateEntry(entry) && entry.source === CLIENT_TOKEN_ESTIMATE_SOURCE && Number.isFinite(entry.tokens) && entry.tokens >= 0; if (matches(cache)) return cache; for (const value of Object.values(cache)) { if (matches(value)) return value; } return void 0; } function getMessageCacheEntry(message, key) { const contentLevelEntry = getCacheEntry(getContentMastraMetadata(message.content)?.tokenEstimate, key); if (contentLevelEntry) return contentLevelEntry; return getCacheEntry(getMessageMastraMetadata(message)?.tokenEstimate, key); } function setMessageCacheEntry(message, key, entry) { const contentMastraMetadata = ensureContentMastraMetadata(message.content); if (contentMastraMetadata) { contentMastraMetadata.tokenEstimate = mergeCacheEntry(contentMastraMetadata.tokenEstimate, key, entry); return; } const messageMastraMetadata = ensureMessageMastraMetadata(message); messageMastraMetadata.tokenEstimate = mergeCacheEntry(messageMastraMetadata.tokenEstimate, key, entry); } function serializePartForTokenCounting(part) { const typedPart = part; const hasTokenEstimate = Boolean(typedPart.providerMetadata?.mastra?.tokenEstimate); if (!hasTokenEstimate) { return JSON.stringify(part); } const clonedPart = { ...typedPart, providerMetadata: { ...typedPart.providerMetadata ?? {}, mastra: { ...typedPart.providerMetadata?.mastra ?? {} } } }; delete clonedPart.providerMetadata.mastra.tokenEstimate; if (Object.keys(clonedPart.providerMetadata.mastra).length === 0) { delete clonedPart.providerMetadata.mastra; } if (Object.keys(clonedPart.providerMetadata).length === 0) { delete clonedPart.providerMetadata; } return JSON.stringify(clonedPart); } function getFilenameFromAttachmentData(data) { const pathname = data instanceof URL ? data.pathname : typeof data === "string" && isHttpUrlString(data) ? (() => { try { return new URL(data).pathname; } catch { return void 0; } })() : void 0; const filename = pathname?.split("/").filter(Boolean).pop(); return filename ? decodeURIComponent(filename) : void 0; } function serializeNonImageFilePartForTokenCounting(part) { const filename = getObjectValue(part, "filename"); const inferredFilename = getFilenameFromAttachmentData(getObjectValue(part, "data")); return JSON.stringify({ type: "file", mimeType: getObjectValue(part, "mimeType") ?? null, filename: typeof filename === "string" && filename.trim().length > 0 ? filename.trim() : inferredFilename ?? null }); } function isValidCacheEntry(entry, expectedKey, expectedSource) { return Boolean( entry && entry.v === TOKEN_ESTIMATE_CACHE_VERSION && entry.source === expectedSource && entry.key === expectedKey && Number.isFinite(entry.tokens) ); } function parseModelContext(model) { if (!model) return void 0; if (typeof model === "object") { return model.provider || model.modelId ? { provider: model.provider, modelId: model.modelId } : void 0; } const slashIndex = model.indexOf("/"); if (slashIndex === -1) { return { modelId: model }; } return { provider: model.slice(0, slashIndex), modelId: model.slice(slashIndex + 1) }; } function normalizeImageDetail(detail) { if (detail === "low" || detail === "high") return detail; return "auto"; } function getObjectValue(value, key) { if (!value || typeof value !== "object") return void 0; return value[key]; } function resolveImageDetail(part) { const openAIProviderOptions = getObjectValue(getObjectValue(part, "providerOptions"), "openai"); const openAIProviderMetadata = getObjectValue(getObjectValue(part, "providerMetadata"), "openai"); const mastraMetadata = getObjectValue(getObjectValue(part, "providerMetadata"), "mastra"); return normalizeImageDetail( getObjectValue(part, "detail") ?? getObjectValue(part, "imageDetail") ?? getObjectValue(openAIProviderOptions, "detail") ?? getObjectValue(openAIProviderOptions, "imageDetail") ?? getObjectValue(openAIProviderMetadata, "detail") ?? getObjectValue(openAIProviderMetadata, "imageDetail") ?? getObjectValue(mastraMetadata, "imageDetail") ); } function normalizeGoogleMediaResolution(value) { return typeof value === "string" && GOOGLE_MEDIA_RESOLUTION_VALUES.has(value) ? value : void 0; } function resolveGoogleMediaResolution(part) { const providerOptions = getObjectValue(getObjectValue(part, "providerOptions"), "google"); const providerMetadata = getObjectValue(getObjectValue(part, "providerMetadata"), "google"); const mastraMetadata = getObjectValue(getObjectValue(part, "providerMetadata"), "mastra"); return normalizeGoogleMediaResolution(getObjectValue(part, "mediaResolution")) ?? normalizeGoogleMediaResolution(getObjectValue(providerOptions, "mediaResolution")) ?? normalizeGoogleMediaResolution(getObjectValue(providerMetadata, "mediaResolution")) ?? normalizeGoogleMediaResolution(getObjectValue(mastraMetadata, "mediaResolution")) ?? "unspecified"; } function getFiniteNumber(value) { return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : void 0; } function isHttpUrlString(value) { return typeof value === "string" && /^https?:\/\//i.test(value); } function isLikelyFilesystemPath(value) { return value.startsWith("/") || value.startsWith("./") || value.startsWith("../") || value.startsWith("~/") || /^[A-Za-z]:[\\/]/.test(value) || value.includes("\\"); } function isLikelyBase64Content(value) { if (value.length < 16 || value.length % 4 !== 0 || /\s/.test(value) || isLikelyFilesystemPath(value)) { return false; } return /^[A-Za-z0-9+/]+={0,2}$/.test(value); } function decodeImageBuffer(value) { if (typeof Buffer !== "undefined" && Buffer.isBuffer(value)) { return value; } if (value instanceof Uint8Array) { return Buffer.from(value); } if (value instanceof ArrayBuffer) { return Buffer.from(value); } if (ArrayBuffer.isView(value)) { return Buffer.from(value.buffer, value.byteOffset, value.byteLength); } if (typeof value !== "string" || isHttpUrlString(value)) { return void 0; } if (value.startsWith("data:")) { const commaIndex = value.indexOf(","); if (commaIndex === -1) return void 0; const header = value.slice(0, commaIndex); const payload = value.slice(commaIndex + 1); if (/;base64/i.test(header)) { return Buffer.from(payload, "base64"); } return Buffer.from(decodeURIComponent(payload), "utf8"); } if (!isLikelyBase64Content(value)) { return void 0; } return Buffer.from(value, "base64"); } function persistImageDimensions(part, dimensions) { const mastraMetadata = ensurePartMastraMetadata(part); mastraMetadata.imageDimensions = dimensions; } function resolveHttpAssetUrl(value) { if (value instanceof URL) { return value.toString(); } if (typeof value === "string" && isHttpUrlString(value)) { return value; } return void 0; } async function resolveImageDimensionsAsync(part) { const existing = resolveImageDimensions(part); if (existing.width && existing.height) { return existing; } const asset = getObjectValue(part, "image") ?? getObjectValue(part, "data"); const url = resolveHttpAssetUrl(asset); if (!url) { return existing; } try { const mod = await import('probe-image-size'); const probeImageSize = mod.default; const probed = await probeImageSize(url, { open_timeout: REMOTE_IMAGE_PROBE_TIMEOUT_MS, response_timeout: REMOTE_IMAGE_PROBE_TIMEOUT_MS, read_timeout: REMOTE_IMAGE_PROBE_TIMEOUT_MS, follow_max: 2 }); const width = existing.width ?? getFiniteNumber(probed.width); const height = existing.height ?? getFiniteNumber(probed.height); if (!width || !height) { return existing; } const resolved = { width, height }; persistImageDimensions(part, resolved); return resolved; } catch { return existing; } } function resolveImageDimensions(part) { const mastraMetadata = getObjectValue(getObjectValue(part, "providerMetadata"), "mastra"); const dimensions = getObjectValue(mastraMetadata, "imageDimensions"); const width = getFiniteNumber(getObjectValue(part, "width")) ?? getFiniteNumber(getObjectValue(part, "imageWidth")) ?? getFiniteNumber(getObjectValue(dimensions, "width")); const height = getFiniteNumber(getObjectValue(part, "height")) ?? getFiniteNumber(getObjectValue(part, "imageHeight")) ?? getFiniteNumber(getObjectValue(dimensions, "height")); if (width && height) { return { width, height }; } const asset = getObjectValue(part, "image") ?? getObjectValue(part, "data"); const buffer = decodeImageBuffer(asset); if (!buffer) { return { width, height }; } try { const measured = imageSize__default.default(buffer); const measuredWidth = getFiniteNumber(measured.width); const measuredHeight = getFiniteNumber(measured.height); if (!measuredWidth || !measuredHeight) { return { width, height }; } const resolved = { width: width ?? measuredWidth, height: height ?? measuredHeight }; persistImageDimensions(part, resolved); return resolved; } catch { return { width, height }; } } function getBase64Size(base64) { const sanitized = base64.replace(/\s+/g, ""); const padding = sanitized.endsWith("==") ? 2 : sanitized.endsWith("=") ? 1 : 0; return Math.max(0, Math.floor(sanitized.length * 3 / 4) - padding); } function resolveImageSourceStats(image) { if (image instanceof URL) { return { source: "url" }; } if (typeof image === "string") { if (isHttpUrlString(image)) { return { source: "url" }; } if (image.startsWith("data:")) { const commaIndex = image.indexOf(","); const encoded = commaIndex === -1 ? "" : image.slice(commaIndex + 1); return { source: "data-uri", sizeBytes: getBase64Size(encoded) }; } return { source: "binary", sizeBytes: getBase64Size(image) }; } if (typeof Buffer !== "undefined" && Buffer.isBuffer(image)) { return { source: "binary", sizeBytes: image.length }; } if (image instanceof Uint8Array) { return { source: "binary", sizeBytes: image.byteLength }; } if (image instanceof ArrayBuffer) { return { source: "binary", sizeBytes: image.byteLength }; } if (ArrayBuffer.isView(image)) { return { source: "binary", sizeBytes: image.byteLength }; } return { source: "binary" }; } function getPathnameExtension(value) { const normalized = value.split("#", 1)[0]?.split("?", 1)[0] ?? value; const match = normalized.match(/\.([a-z0-9]+)$/i); return match?.[1]?.toLowerCase(); } function hasImageFilenameExtension(filename) { return typeof filename === "string" && IMAGE_FILE_EXTENSIONS.has(getPathnameExtension(filename) ?? ""); } function isImageLikeFilePart(part) { if (getObjectValue(part, "type") !== "file") { return false; } const mimeType = getObjectValue(part, "mimeType"); if (typeof mimeType === "string" && mimeType.toLowerCase().startsWith("image/")) { return true; } const data = getObjectValue(part, "data"); if (typeof data === "string" && data.startsWith("data:image/")) { return true; } if (data instanceof URL && hasImageFilenameExtension(data.pathname)) { return true; } if (isHttpUrlString(data)) { try { const url = new URL(data); if (hasImageFilenameExtension(url.pathname)) { return true; } } catch { } } return hasImageFilenameExtension(getObjectValue(part, "filename")); } function resolveProviderId(modelContext) { return modelContext?.provider?.toLowerCase(); } function resolveModelId(modelContext) { return modelContext?.modelId?.toLowerCase() ?? ""; } function resolveOpenAIImageEstimatorConfig(modelContext) { const modelId = resolveModelId(modelContext); if (modelId.startsWith("gpt-5") || modelId === "gpt-5-chat-latest") { return { baseTokens: 70, tileTokens: 140, fallbackTiles: 4 }; } if (modelId.startsWith("gpt-4o-mini")) { return { baseTokens: 2833, tileTokens: 5667, fallbackTiles: 1 }; } if (modelId.startsWith("o1") || modelId.startsWith("o3")) { return { baseTokens: 75, tileTokens: 150, fallbackTiles: 4 }; } if (modelId.includes("computer-use")) { return { baseTokens: 65, tileTokens: 129, fallbackTiles: 4 }; } return DEFAULT_IMAGE_ESTIMATOR; } function isGoogleGemini3Model(modelContext) { return resolveProviderId(modelContext) === "google" && resolveModelId(modelContext).startsWith("gemini-3"); } function scaleDimensionsForOpenAIHighDetail(width, height) { let scaledWidth = width; let scaledHeight = height; const largestSide = Math.max(scaledWidth, scaledHeight); if (largestSide > 2048) { const ratio = 2048 / largestSide; scaledWidth *= ratio; scaledHeight *= ratio; } const shortestSide = Math.min(scaledWidth, scaledHeight); if (shortestSide > 768) { const ratio = 768 / shortestSide; scaledWidth *= ratio; scaledHeight *= ratio; } return { width: Math.max(1, Math.round(scaledWidth)), height: Math.max(1, Math.round(scaledHeight)) }; } function scaleDimensionsForAnthropic(width, height) { const largestSide = Math.max(width, height); if (largestSide <= ANTHROPIC_IMAGE_MAX_LONG_EDGE) { return { width, height }; } const ratio = ANTHROPIC_IMAGE_MAX_LONG_EDGE / largestSide; return { width: Math.max(1, Math.round(width * ratio)), height: Math.max(1, Math.round(height * ratio)) }; } function estimateOpenAIHighDetailTiles(dimensions, sourceStats, estimator) { if (dimensions.width && dimensions.height) { const scaled = scaleDimensionsForOpenAIHighDetail(dimensions.width, dimensions.height); return Math.max(1, Math.ceil(scaled.width / 512) * Math.ceil(scaled.height / 512)); } if (sourceStats.sizeBytes !== void 0) { if (sourceStats.sizeBytes <= 512 * 1024) return 1; if (sourceStats.sizeBytes <= 2 * 1024 * 1024) return 4; if (sourceStats.sizeBytes <= 4 * 1024 * 1024) return 6; return 8; } return estimator.fallbackTiles; } function resolveEffectiveOpenAIImageDetail(detail, dimensions, sourceStats) { if (detail === "low" || detail === "high") return detail; if (dimensions.width && dimensions.height) { return Math.max(dimensions.width, dimensions.height) > 768 ? "high" : "low"; } if (sourceStats.sizeBytes !== void 0) { return sourceStats.sizeBytes > 1024 * 1024 ? "high" : "low"; } return "low"; } function estimateLegacyGoogleImageTiles(dimensions) { if (!dimensions.width || !dimensions.height) return 1; return Math.max(1, Math.ceil(dimensions.width / 768) * Math.ceil(dimensions.height / 768)); } function estimateAnthropicImageTokens(dimensions, sourceStats) { if (dimensions.width && dimensions.height) { const scaled = scaleDimensionsForAnthropic(dimensions.width, dimensions.height); return Math.max(1, Math.ceil(scaled.width * scaled.height * ANTHROPIC_IMAGE_TOKENS_PER_PIXEL)); } if (sourceStats.sizeBytes !== void 0) { if (sourceStats.sizeBytes <= 512 * 1024) return 341; if (sourceStats.sizeBytes <= 2 * 1024 * 1024) return 1366; if (sourceStats.sizeBytes <= 4 * 1024 * 1024) return 2048; return 2731; } return 1600; } function estimateFileTokensFromBytes(provider, mimeType, sizeBytes) { const normalizedMime = (mimeType ?? "").toLowerCase().split(";", 1)[0].trim(); const isPdf = normalizedMime === "application/pdf"; const isTextish = normalizedMime.startsWith("text/") || ["application/json", "application/xml", "application/x-yaml", "application/yaml"].includes(normalizedMime); if (isPdf) { if (provider === "google") return Math.max(258, Math.ceil(sizeBytes / 20)); if (provider === "anthropic") return Math.max(1500, Math.ceil(sizeBytes / 3)); return Math.max(500, Math.ceil(sizeBytes / 4)); } if (isTextish) return Math.max(1, Math.ceil(sizeBytes / 4)); return Math.max(1, Math.ceil(sizeBytes / 4)); } function estimateNonImageFileTokens(modelContext, part) { const sourceStats = resolveImageSourceStats(getObjectValue(part, "data")); if (sourceStats.sizeBytes === void 0) { return void 0; } const provider = resolveProviderId(modelContext); const modelId = modelContext?.modelId ?? null; const mimeType = getAttachmentMimeType(part, "application/octet-stream"); const filename = getAttachmentFilename(part) ?? null; const tokens = estimateFileTokensFromBytes(provider, mimeType, sourceStats.sizeBytes); return { tokens, cachePayload: JSON.stringify({ kind: "non-image-file", provider: provider ?? "fallback", modelId, estimator: "bytes", source: sourceStats.source, sizeBytes: sourceStats.sizeBytes, mimeType, filename }) }; } function estimateGoogleImageTokens(modelContext, part, dimensions) { if (isGoogleGemini3Model(modelContext)) { const mediaResolution = resolveGoogleMediaResolution(part); return { tokens: GOOGLE_GEMINI_3_IMAGE_TOKENS_BY_RESOLUTION[mediaResolution], mediaResolution }; } return { tokens: estimateLegacyGoogleImageTiles(dimensions) * GOOGLE_LEGACY_IMAGE_TOKENS_PER_TILE, mediaResolution: "unspecified" }; } function getProviderApiKey(provider) { for (const envVar of PROVIDER_API_KEY_ENV_VARS[provider] ?? []) { const value = process.env[envVar]; if (typeof value === "string" && value.trim().length > 0) { return value.trim(); } } return void 0; } function getAttachmentFilename(part) { const explicitFilename = getObjectValue(part, "filename"); if (typeof explicitFilename === "string" && explicitFilename.trim().length > 0) { return explicitFilename.trim(); } return getFilenameFromAttachmentData(getObjectValue(part, "data") ?? getObjectValue(part, "image")); } function getAttachmentMimeType(part, fallback) { const mimeType = getObjectValue(part, "mimeType"); if (typeof mimeType === "string" && mimeType.trim().length > 0) { return mimeType.trim(); } const asset = getObjectValue(part, "data") ?? getObjectValue(part, "image"); if (typeof asset === "string" && asset.startsWith("data:")) { const semicolonIndex = asset.indexOf(";"); const commaIndex = asset.indexOf(","); const endIndex = semicolonIndex === -1 ? commaIndex : Math.min(semicolonIndex, commaIndex); if (endIndex > 5) { return asset.slice(5, endIndex); } } return fallback; } function getAttachmentUrl(asset) { if (asset instanceof URL) { return asset.toString(); } if (typeof asset === "string" && /^(https?:\/\/|data:)/i.test(asset)) { return asset; } return void 0; } function getAttachmentFingerprint(asset) { const url = getAttachmentUrl(asset); if (url) { return { url }; } const base64 = encodeAttachmentBase64(asset); if (base64) { return { contentHash: crypto$1.createHash("sha256").update(base64).digest("hex") }; } return {}; } function encodeAttachmentBase64(asset) { if (typeof asset === "string") { if (asset.startsWith("data:")) { const commaIndex = asset.indexOf(","); return commaIndex === -1 ? void 0 : asset.slice(commaIndex + 1); } if (/^https?:\/\//i.test(asset)) { return void 0; } return asset; } if (typeof Buffer !== "undefined" && Buffer.isBuffer(asset)) { return asset.toString("base64"); } if (asset instanceof Uint8Array) { return Buffer.from(asset).toString("base64"); } if (asset instanceof ArrayBuffer) { return Buffer.from(asset).toString("base64"); } if (ArrayBuffer.isView(asset)) { return Buffer.from(asset.buffer, asset.byteOffset, asset.byteLength).toString("base64"); } return void 0; } function createTimeoutSignal(timeoutMs) { const controller = new AbortController(); const timeout = setTimeout( () => controller.abort(new Error(`Attachment token counting timed out after ${timeoutMs}ms`)), timeoutMs ); const cleanup = () => clearTimeout(timeout); controller.signal.addEventListener("abort", cleanup, { once: true }); return { signal: controller.signal, cleanup }; } function getNumericResponseField(value, paths) { for (const path of paths) { let current = value; for (const segment of path) { current = getObjectValue(current, segment); if (current === void 0) break; } if (typeof current === "number" && Number.isFinite(current)) { return current; } } return void 0; } function toOpenAIInputPart(part) { if (getObjectValue(part, "type") === "image" || isImageLikeFilePart(part)) { const asset = getObjectValue(part, "image") ?? getObjectValue(part, "data"); const imageUrl = getAttachmentUrl(asset); if (imageUrl) { return { type: "input_image", image_url: imageUrl, detail: resolveImageDetail(part) }; } const base64 = encodeAttachmentBase64(asset); if (!base64) return void 0; return { type: "input_image", image_url: `data:${getAttachmentMimeType(part, "image/png")};base64,${base64}`, detail: resolveImageDetail(part) }; } if (getObjectValue(part, "type") === "file") { const asset = getObjectValue(part, "data"); const fileUrl = getAttachmentUrl(asset); return fileUrl ? { type: "input_file", file_url: fileUrl, filename: getAttachmentFilename(part) ?? "attachment" } : (() => { const base64 = encodeAttachmentBase64(asset); if (!base64) return void 0; return { type: "input_file", file_data: `data:${getAttachmentMimeType(part, "application/octet-stream")};base64,${base64}`, filename: getAttachmentFilename(part) ?? "attachment" }; })(); } return void 0; } function toAnthropicContentPart(part) { const asset = getObjectValue(part, "image") ?? getObjectValue(part, "data"); const url = getAttachmentUrl(asset); if (getObjectValue(part, "type") === "image" || isImageLikeFilePart(part)) { return url && /^https?:\/\//i.test(url) ? { type: "image", source: { type: "url", url } } : (() => { const base64 = encodeAttachmentBase64(asset); if (!base64) return void 0; return { type: "image", source: { type: "base64", media_type: getAttachmentMimeType(part, "image/png"), data: base64 } }; })(); } if (getObjectValue(part, "type") === "file") { return url && /^https?:\/\//i.test(url) ? { type: "document", source: { type: "url", url } } : (() => { const base64 = encodeAttachmentBase64(asset); if (!base64) return void 0; return { type: "document", source: { type: "base64", media_type: getAttachmentMimeType(part, "application/pdf"), data: base64 } }; })(); } return void 0; } function toGooglePart(part) { const asset = getObjectValue(part, "image") ?? getObjectValue(part, "data"); const url = getAttachmentUrl(asset); const mimeType = getAttachmentMimeType( part, getObjectValue(part, "type") === "file" && !isImageLikeFilePart(part) ? "application/pdf" : "image/png" ); if (url && !url.startsWith("data:")) { return { fileData: { mimeType, fileUri: url } }; } const base64 = encodeAttachmentBase64(asset); if (!base64) return void 0; return { inlineData: { mimeType, data: base64 } }; } async function fetchOpenAIAttachmentTokenEstimate(modelId, part) { const apiKey = getProviderApiKey("openai"); const inputPart = toOpenAIInputPart(part); if (!apiKey || !inputPart) return void 0; const { signal, cleanup } = createTimeoutSignal(ATTACHMENT_COUNT_TIMEOUT_MS); try { const response = await fetch("https://api.openai.com/v1/responses/input_tokens", { method: "POST", headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" }, body: JSON.stringify({ model: modelId, input: [{ type: "message", role: "user", content: [inputPart] }] }), signal }); if (!response.ok) return void 0; const body = await response.json(); return getNumericResponseField(body, [ ["input_tokens"], ["total_tokens"], ["usage", "input_tokens"], ["usage", "total_tokens"] ]); } finally { cleanup(); } } async function fetchAnthropicAttachmentTokenEstimate(modelId, part) { const apiKey = getProviderApiKey("anthropic"); const contentPart = toAnthropicContentPart(part); if (!apiKey || !contentPart) return void 0; const { signal, cleanup } = createTimeoutSignal(ATTACHMENT_COUNT_TIMEOUT_MS); try { const response = await fetch("https://api.anthropic.com/v1/messages/count_tokens", { method: "POST", headers: { "x-api-key": apiKey, "anthropic-version": "2023-06-01", "Content-Type": "application/json" }, body: JSON.stringify({ model: modelId, messages: [{ role: "user", content: [contentPart] }] }), signal }); if (!response.ok) return void 0; const body = await response.json(); return getNumericResponseField(body, [["input_tokens"]]); } finally { cleanup(); } } async function fetchGoogleAttachmentTokenEstimate(modelId, part) { const apiKey = getProviderApiKey("google"); const googlePart = toGooglePart(part); if (!apiKey || !googlePart) return void 0; const { signal, cleanup } = createTimeoutSignal(ATTACHMENT_COUNT_TIMEOUT_MS); try { const response = await fetch(`https://generativelanguage.googleapis.com/v1beta/models/${modelId}:countTokens`, { method: "POST", headers: { "x-goog-api-key": apiKey, "Content-Type": "application/json" }, body: JSON.stringify({ contents: [{ role: "user", parts: [googlePart] }] }), signal }); if (!response.ok) return void 0; const body = await response.json(); return getNumericResponseField(body, [["totalTokens"], ["total_tokens"]]); } finally { cleanup(); } } var TokenCounter = class _TokenCounter { cacheSource; defaultModelContext; modelContextStorage = new async_hooks.AsyncLocalStorage(); inFlightAttachmentCounts = /* @__PURE__ */ new Map(); // Per-message overhead: accounts for role tokens, message framing, and separators. // 3.8 remains a practical average across providers for OM thresholding. static TOKENS_PER_MESSAGE = 3.8; // Conversation-level overhead: system prompt framing, reply priming tokens, etc. static TOKENS_PER_CONVERSATION = 24; constructor(options) { this.cacheSource = `v${TOKEN_ESTIMATE_CACHE_VERSION}:${resolveEstimatorId()}`; this.defaultModelContext = parseModelContext(options?.model); } runWithModelContext(model, fn) { return this.modelContextStorage.run(parseModelContext(model), fn); } getModelContext() { return this.modelContextStorage.getStore() ?? this.defaultModelContext; } /** * Count tokens in a plain string */ countString(text) { if (!text) return 0; return tokenx.estimateTokenCount(text); } readOrPersistPartEstimate(part, kind, payload) { const key = buildEstimateKey(kind, payload); const cached = getPartCacheEntry(part, key); if (isValidCacheEntry(cached, key, this.cacheSource)) { return cached.tokens; } const tokens = this.countString(payload); setPartCacheEntry(part, key, { v: TOKEN_ESTIMATE_CACHE_VERSION, source: this.cacheSource, key, tokens }); return tokens; } readOrPersistFixedPartEstimate(part, kind, payload, tokens) { const key = buildEstimateKey(kind, payload); const cached = getPartCacheEntry(part, key); if (isValidCacheEntry(cached, key, this.cacheSource)) { return cached.tokens; } setPartCacheEntry(part, key, { v: TOKEN_ESTIMATE_CACHE_VERSION, source: this.cacheSource, key, tokens }); return tokens; } readOrPersistMessageEstimate(message, kind, payload) { const key = buildEstimateKey(kind, payload); const cached = getMessageCacheEntry(message, key); if (isValidCacheEntry(cached, key, this.cacheSource)) { return cached.tokens; } const tokens = this.countString(payload); setMessageCacheEntry(message, key, { v: TOKEN_ESTIMATE_CACHE_VERSION, source: this.cacheSource, key, tokens }); return tokens; } resolveToolResultForTokenCounting(part, invocationResult) { return resolveToolResultValue(part, invocationResult); } countMultimodalToolResultContent(part, toolResult) { if (!toolResult || typeof toolResult !== "object") { return void 0; } const output = toolResult; const content = output.type === "content" && Array.isArray(output.value) ? output.value : output.content; if (!Array.isArray(content)) { return void 0; } let hasAttachment = false; let tokens = 0; const cacheParts = []; const countJsonContentPart = (contentPart) => { const formatted = formatToolResultForObserver(contentPart); tokens += this.countString(formatted); cacheParts.push({ type: "json", valueHash: crypto$1.createHash("sha256").update(formatted).digest("hex") }); }; for (const item of content) { if (!item || typeof item !== "object") { continue; } const contentPart = item; const partType = contentPart.type; if (partType === "text") { const text = typeof contentPart.text === "string" ? contentPart.text : String(contentPart.value ?? ""); tokens += this.countString(text); cacheParts.push({ type: "text", textHash: crypto$1.createHash("sha256").update(text).digest("hex") }); continue; } if (partType === "image" || partType === "image-data" || partType === "media" && String(contentPart.mediaType ?? "").startsWith("image/")) { if (typeof contentPart.data !== "string") { countJsonContentPart(contentPart); continue; } hasAttachment = true; const imagePart = { type: "image", image: contentPart.data, mimeType: contentPart.mediaType ?? contentPart.mimeType, providerOptions: contentPart.providerOptions, providerMetadata: contentPart.providerMetadata }; const clientEstimate = getClientPartTokenEstimate(imagePart); if (clientEstimate) { tokens += clientEstimate.tokens; cacheParts.push({ type: "image-data-client-estimate", key: clientEstimate.key, tokens: clientEstimate.tokens }); continue; } const estimate = this.estimateImageTokens(imagePart); tokens += estimate.tokens; cacheParts.push({ type: "image-data", estimate: JSON.parse(estimate.cachePayload) }); continue; } if (partType === "audio" || partType === "file-data" || partType === "media") { if (typeof contentPart.data !== "string") { countJsonContentPart(contentPart); continue; } hasAttachment = true; const filePart = { type: "file", data: contentPart.data, mimeType: contentPart.mediaType ?? contentPart.mimeType, filename: contentPart.filename, providerOptions: contentPart.providerOptions, providerMetadata: contentPart.providerMetadata }; const clientEstimate = getClientPartTokenEstimate(filePart); if (clientEstimate) { tokens += clientEstimate.tokens; cacheParts.push({ type: "file-data-client-estimate", key: clientEstimate.key, tokens: clientEstimate.tokens }); continue; } if (isImageLikeFilePart(filePart)) { const estimate = this.estimateImageLikeFileTokens(filePart); tokens += estimate.tokens; cacheParts.push({ type: "image-like-file-data", estimate: JSON.parse(estimate.cachePayload) }); continue; } const byteEstimate = estimateNonImageFileTokens(this.getModelContext(), filePart); if (byteEstimate) { tokens += byteEstimate.tokens; cacheParts.push({ type: "file-data", estimate: JSON.parse(byteEstimate.cachePayload) }); continue; } const descriptor = serializeNonImageFilePartForTokenCounting(filePart); tokens += this.countString(descriptor); cacheParts.push({ type: "file-data-descriptor", descriptor }); continue; } countJsonContentPart(contentPart); } if (!hasAttachment) { return void 0; } return this.readOrPersistFixedPartEstimate( part, "tool-result-multimodal-content", JSON.stringify({ type: "content", value: cacheParts }), tokens ); } estimateImageAssetTokens(part, asset, kind) { const modelContext = this.getModelContext(); const provider = resolveProviderId(modelContext); const modelId = modelContext?.modelId ?? null; const detail = resolveImageDetail(part); const dimensions = resolveImageDimensions(part); const sourceStats = resolveImageSourceStats(asset); if (provider === "google") { const googleEstimate = estimateGoogleImageTokens(modelContext, part, dimensions); return { tokens: googleEstimate.tokens, cachePayload: JSON.stringify({ kind, provider, modelId, estimator: isGoogleGemini3Model(modelContext) ? "google-gemini-3" : "google-legacy", mediaResolution: googleEstimate.mediaResolution, width: dimensions.width ?? null, height: dimensions.height ?? null, source: sourceStats.source, sizeBytes: sourceStats.sizeBytes ?? null, mimeType: getObjectValue(part, "mimeType") ?? null, filename: getObjectValue(part, "filename") ?? null }) }; } if (provider === "anthropic") { return { tokens: estimateAnthropicImageTokens(dimensions, sourceStats), cachePayload: JSON.stringify({ kind, provider, modelId, estimator: "anthropic", width: dimensions.width ?? null, height: dimensions.height ?? null, source: sourceStats.source, sizeBytes: sourceStats.sizeBytes ?? null, mimeType: getObjectValue(part, "mimeType") ?? null, filename: getObjectValue(part, "filename") ?? null }) }; } const estimator = resolveOpenAIImageEstimatorConfig(modelContext); const effectiveDetail = resolveEffectiveOpenAIImageDetail(detail, dimensions, sourceStats); const tiles = effectiveDetail === "high" ? estimateOpenAIHighDetailTiles(dimensions, sourceStats, estimator) : 0; const tokens = estimator.baseTokens + tiles * estimator.tileTokens; return { tokens, cachePayload: JSON.stringify({ kind, provider, modelId, estimator: provider === "openai" ? "openai" : "fallback", detail, effectiveDetail, width: dimensions.width ?? null, height: dimensions.height ?? null, source: sourceStats.source, sizeBytes: sourceStats.sizeBytes ?? null, mimeType: getObjectValue(part, "mimeType") ?? null, filename: getObjectValue(part, "filename") ?? null }) }; } estimateImageTokens(part) { return this.estimateImageAssetTokens(part, part.image, "image"); } estimateImageLikeFileTokens(part) { return this.estimateImageAssetTokens(part, part.data, "file"); } countAttachmentPartSync(part) { if (part.type === "image" || part.type === "file") { const clientEstimate = getClientPartTokenEstimate(part); if (clientEstimate) { return clientEstimate.tokens; } } if (part.type === "image") { const estimate = this.estimateImageTokens(part); return this.readOrPersistFixedPartEstimate(part, "image", estimate.cachePayload, estimate.tokens); } if (part.type === "file" && isImageLikeFilePart(part)) { const estimate = this.estimateImageLikeFileTokens(part); return this.readOrPersistFixedPartEstimate(part, "image-like-file", estimate.cachePayload, estimate.tokens); } if (part.type === "file") { const byteEstimate = estimateNonImageFileTokens(this.getModelContext(), part); if (byteEstimate) { return this.readOrPersistFixedPartEstimate( part, "non-image-file", byteEstimate.cachePayload, byteEstimate.tokens ); } return this.readOrPersistPartEstimate(part, "file-descriptor", serializeNonImageFilePartForTokenCounting(part)); } return void 0; } buildRemoteAttachmentCachePayload(part) { const isImageAttachment = part.type === "image" || part.type === "file" && isImageLikeFilePart(part); const isNonImageFileAttachment = part.type === "file" && !isImageAttachment; if (!isImageAttachment && !isNonImageFileAttachment) { return void 0; } const modelContext = this.getModelContext(); const provider = resolveProviderId(modelContext); const modelId = modelContext?.modelId ?? null; if (!provider || !modelId || !["openai", "google", "anthropic"].includes(provider)) { return void 0; } const asset = getObjectValue(part, "image") ?? getObjectValue(part, "data"); const sourceStats = resolveImageSourceStats(asset); const fingerprint = getAttachmentFingerprint(asset); return JSON.stringify({ strategy: "provider-endpoint", provider, modelId, type: getObjectValue(part, "type") ?? null, detail: isImageAttachment ? resolveImageDetail(part) : null, mediaResolution: provider === "google" && isImageAttachment ? resolveGoogleMediaResolution(part) : null, mimeType: getAttachmentMimeType(part, isNonImageFileAttachment ? "application/pdf" : "image/png"), filename: getAttachmentFilename(part) ?? null, source: sourceStats.source, sizeBytes: sourceStats.sizeBytes ?? null, assetUrl: fingerprint.url ?? null, assetHash: fingerprint.contentHash ?? null }); } async fetchProviderAttachmentTokenEstimate(part) { const modelContext = this.getModelContext(); const provider = resolveProviderId(modelContext); const modelId = modelContext?.modelId; if (!provider || !modelId) return void 0; try { if (provider === "openai") { return await fetchOpenAIAttachmentTokenEstimate(modelId, part); } if (provider === "google") { return await fetchGoogleAttachmentTokenEstimate(modelId, part); } if (provider === "anthropic") { return await fetchAnthropicAttachmentTokenEstimate(modelId, part); } } catch { return void 0; } return void 0; } async countAttachmentPartAsync(part) { if (part.type === "image" || part.type === "file") { const clientEstimate = getClientPartTokenEstimate(part); if (clientEstimate) { return clientEstimate.tokens; } } const isImageAttachment = part.type === "image" || part.type === "file" && isImageLikeFilePart(part); const remotePayload = this.buildRemoteAttachmentCachePayload(part); if (remotePayload) { const remoteKey = buildEstimateKey("attachment-provider", remotePayload); const cachedRemote = getPartCacheEntry(part, remoteKey); if (isValidCacheEntry(cachedRemote, remoteKey, this.cacheSource)) { return cachedRemote.tokens; } const existingRequest = this.inFlightAttachmentCounts.get(remoteKey); if (existingRequest) { const remoteTokens = await existingRequest; if (typeof remoteTokens === "number" && Number.isFinite(remoteTokens) && remoteTokens > 0) { setPartCacheEntry(part, remoteKey, { v: TOKEN_ESTIMATE_CACHE_VERSION, source: this.cacheSource, key: remoteKey, tokens: remoteTokens }); return remoteTokens; } } else { const remoteRequest = this.fetchProviderAttachmentTokenEstimate(part); this.inFlightAttachmentCounts.set(remoteKey, remoteRequest); let remoteTokens; try { remoteTokens = await remoteRequest; } finally { this.inFlightAttachmentCounts.delete(remoteKey); } if (typeof remoteTokens === "number" && Number.isFinite(remoteTokens) && remoteTokens > 0) { setPartCacheEntry(part, remoteKey, { v: TOKEN_ESTIMATE_CACHE_VERSION, source: this.cacheSource, key: remoteKey, tokens: remoteTokens }); return remoteTokens; } } if (isImageAttachment) { await resolveImageDimensionsAsync(part); } const fallbackPayload = JSON.stringify({ ...JSON.parse(remotePayload), strategy: "local-fallback", ...isImageAttachment ? resolveImageDimensions(part) : {} }); const fallbackKey = buildEstimateKey("attachment-provider", fallbackPayload); const cachedFallback = getPartCacheEntry(part, fallbackKey); if (isValidCacheEntry(cachedFallback, fallbackKey, this.cacheSource)) { return cachedFallback.tokens; } const localTokens2 = this.countAttachmentPartSync(part); if (localTokens2 === void 0) { return void 0; } setPartCacheEntry(part, fallbackKey, { v: TOKEN_ESTIMATE_CACHE_VERSION, source: this.cacheSource, key: fallbackKey, tokens: localTokens2 }); return localTokens2; } if (isImageAttachment) { await resolveImageDimensionsAsync(part); } const localTokens = this.countAttachmentPartSync(part); return localTokens; } countNonAttachmentPart(part) { let overheadDelta = 0; let toolResultDelta = 0; if (part.type === "text") { return { tokens: this.readOrPersistPartEstimate(part, "text", part.text), overheadDelta, toolResultDelta }; } if (part.type === "tool-invocation") { const invocation = part.toolInvocation; let tokens = 0; if (invocation.state === "call" || invocation.state === "partial-call") { if (invocation.toolName) { tokens += this.readOrPersistPartEstimate(part, `tool-${invocation.state}-name`, invocation.toolName); } if (invocation.args) { if (typeof invocation.args === "string") { tokens += this.readOrPersistPartEstimate(part, `tool-${invocation.state}-args`, invocation.args); } else { const argsJson = JSON.stringify(invocation.args); tokens += this.readOrPersistPartEstimate(part, `tool-${invocation.state}-args-json`, argsJson); overheadDelta -= 12; } } return { tokens, overheadDelta, toolResultDelta }; } if (invocation.state === "result") { toolResultDelta++; const { value: resultForCounting, usingStoredModelOutput } = this.resolveToolResultForTokenCounting( part, invocation.result ); if (resultForCounting !== void 0) { const contentTokens = this.countMultimodalToolResultContent(part, resultForCounting); if (contentTokens !== void 0) { tokens += contentTokens; } else { const formattedResult = formatToolResultForObserver(resultForCounting); tokens += this.readOrPersistPartEstimate( part, usingStoredModelOutput ? "tool-result-model-output-json" : "tool-result-json", formattedResult ); } if (typeof resultForCounting !== "string") { overheadDelta -= 12; } } return { tokens, overheadDelta, toolResultDelta }; } throw new Error( `Unhandled tool-invocation state '${part.toolInvocation?.state}' in token counting for part type '${part.type}'` ); } if (typeof part.type === "string" && part.type.startsWith("data-")) { return { tokens: 0, overheadDelta, toolResultDelta }; } if (part.type === "reasoning") { return { tokens: 0, overheadDelta, toolResultDelta }; } const serialized = serializePartForTokenCounting(part); return { tokens: this.readOrPersistPartEstimate(part, `part-${part.type}`, serialized), overheadDelta, toolResultDelta }; } /** * Count tokens in a single message */ countMessage(message) { let payloadTokens = this.countString(message.role); let overhead = _TokenCounter.TOKENS_PER_MESSAGE; let toolResultCount = 0; if (typeof message.content === "string") { payloadTokens += this.readOrPersistMessageEstimate(message, "message-content", message.content); } else if (message.content && typeof message.content === "object") { if (message.content.content && !Array.isArray(message.content.parts)) { payloadTokens += this.readOrPersistMessageEstimate(message, "content-content", message.content.content); } else if (Array.isArray(message.content.parts)) { for (const part of message.content.parts) { const attachmentTokens = this.countAttachmentPartSync(part); if (attachmentTokens !== void 0) { payloadTokens += attachmentTokens; continue; } const result = this.countNonAttachmentPart(part); payloadTokens += result.tokens; overhead += result.overheadDelta; toolResultCount += result.toolResultDelta; } } } if (toolResultCount > 0) { overhead += toolResultCount * _TokenCounter.TOKENS_PER_MESSAGE; } return Math.round(payloadTokens + overhead); } async countMessageAsync(message) { let payloadTokens = this.countString(message.role); let overhead = _TokenCounter.TOKENS_PER_MESSAGE; let toolResultCount = 0; if (typeof message.content === "string") { payloadTokens += this.readOrPersistMessageEstimate(message, "message-content", message.content); } else if (message.content && typeof message.content === "object") { if (message.content.content && !Array.isArray(message.content.parts)) { payloadTokens += this.readOrPersistMessageEstimate(message, "content-content", message.content.content); } else if (Array.isArray(message.content.parts)) { for (const part of message.content.parts) { const attachmentTokens = await this.countAttachmentPartAsync(part); if (attachmentTokens !== void 0) { payloadTokens += attachmentTokens; continue; } const result = this.countNonAttachmentPart(part); payloadTokens += result.tokens; overhead += result.overheadDelta; toolResultCount += result.toolResultDelta; } } } if (toolResultCount > 0) { overhead += toolResultCount * _TokenCounter.TOKENS_PER_MESSAGE; } return Math.round(payloadTokens + overhead); } /** * Count tokens in an array of messages */ countMessages(messages) { if (!messages || messages.length === 0) return 0; let total = _TokenCounter.TOKENS_PER_CONVERSATION; for (const message of messages) { total += this.countMessage(message); } return total; } async countMessagesAsync(messages) { if (!messages || messages.length === 0) return 0; const messageTotals = await Promise.all(messages.map((message) => this.countMessageAsync(message))); return _TokenCounter.TOKENS_PER_CONVERSATION + messageTotals.reduce((sum, count) => sum + count, 0); } /** * Count tokens in observations string */ countObservations(observations) { return this.countString(observations); } }; // src/processors/observational-memory/observational-memory.ts function getLatestStepParts(parts) { for (let i = parts.length - 1; i >= 0; i--) { if (parts[i]?.type === "step-start") { return parts.slice(i + 1); } } return parts; } function messageHasVisibleContent(msg) { const content = msg.content; if (content?.parts && Array.isArray(content.parts)) { return content.parts.some((p) => { const t = p?.type; return t && !t.startsWith("data-") && t !== "step-start"; }); } if (content?.content) return true; return false; } function buildMessageRange(messages) { const first = messages.find(messageHasVisibleContent) ?? messages[0]; const last = [...messages].reverse().find(messageHasVisibleContent) ?? messages[messages.length - 1]; return `${first.id}:${last.id}`; } function getLastActivityFromMessages(messages) { if (!messages) return void 0; for (let i = messages.length - 1; i >= 0; i--) { const message = messages[i]; if (!message || message.role !== "assistant") { continue; } if (!message.content || typeof message.content === "string") { return message.createdAt ? new Date(message.createdAt).getTime() : void 0; } for (let j = message.content.parts.length - 1; j >= 0; j--) { const part = message.content.parts[j]; if (!part || part.type?.startsWith("data-")) { continue; } if (part.createdAt !== void 0) { return part.createdAt; } } return message.createdAt ? new Date(message.createdAt).getTime() : void 0; } return void 0; } function formatModelContext2(provider, modelId) { if (provider && modelId) { return `${provider}/${modelId}`; } return modelId; } function getLastModelFromMessages(messages) { if (!messages) return void 0; for (let i = messages.length - 1; i >= 0; i--) { const message = messages[i]; if (!message || message.role !== "assistant" || !message.content || typeof message.content === "string") { continue; } for (let j = message.content.parts.length - 1; j >= 0; j--) { const part = message.content.parts[j]; if (part?.type === "step-start" && typeof part.model === "string" && part.model.length > 0) { return part.model; } } const metadata = message.content.metadata; const model = formatModelContext2(metadata?.provider, metadata?.modelId); if (model) { return model; } } return void 0; } function getCurrentModel2(model) { return formatModelContext2(model?.provider, model?.modelId); } function parseActivationTTL(value, fieldPath) { if (value === void 0 || value === false) { return void 0; } if (value === "auto") { return value; } if (typeof value === "number") { if (!Number.isFinite(value) || value < 0) { throw new Error(`${fieldPath} must be a non-negative number of milliseconds or a duration string like "5m".`); } return value; } const trimmed = value.trim(); const match = trimmed.match( /^(\d+(?:\.\d+)?)\s*(ms|msec|msecs|millisecond|milliseconds|s|sec|secs|second|seconds|m|min|mins|minute|minutes|h|hr|hrs|hour|hours)$/i ); if (!match) { throw new Error( `${fieldPath} must be a non-negative number of milliseconds or a duration string like "5m" or "1hr".` ); } const rawAmount = match[1]; const rawUnit = match[2]; const amount = Number(rawAmount); const unit = rawUnit.toLowerCase(); if (!Number.isFinite(amount) || amount < 0) { throw new Error(`${fieldPath} must be a non-negative number of milliseconds or a duration string like "5m".`); } const multiplier = unit === "ms" || unit === "msec" || unit === "msecs" || unit === "millisecond" || unit === "milliseconds" ? 1 : unit === "s" || unit === "sec" || unit === "secs" || unit === "second" || unit === "seconds" ? 1e3 : unit === "m" || unit === "min" || unit === "mins" || unit === "minute" || unit === "minutes" ? 6e4 : 36e5; return amount * multiplier; } var ObservationalMemory = class _ObservationalMemory { storage; tokenCounter; scope; /** Whether retrieval-mode observation groups are enabled. */ retrieval; observationConfig; reflectionConfig; onDebugEvent; onIndexObservations; /** Observer agent runner — handles LLM calls for extracting observations. */ observer; /** Reflector agent runner — handles LLM calls for compressing observations. */ reflector; /** Buffering state coordinator — manages static maps and buffering lifecycle. */ buffering; shouldObscureThreadIds = false; hasher = xxhash__default.default(); mastra; /** * Track message IDs observed during this instance's lifetime. * Prevents re-observing messages when per-thread lastObservedAt cursors * haven't fully advanced past messages observed in a prior cycle. * @internal Used by observation strategies. Do not call directly. */ observedMessageIds = /* @__PURE__ */ new Set(); /** Internal MessageHistory for message persistence */ messageHistory; /** * In-memory mutex for serializing observation/reflection cycles per resource/thread. * Prevents race conditions where two concurrent cycles could both read isObserving=false * before either sets it to true, leading to lost work. * * Key format: "resource:{resourceId}" or "thread:{threadId}" * Value: Promise that resolves when the lock is released * * NOTE: This mutex only works within a single Node.js process. For distributed * deployments, external locking (Redis, database locks) would be needed, or * accept eventual consistency (acceptable for v1). */ locks = /* @__PURE__ */ new Map(); /** * Acquire a lock for the given key, execute the callback, then release. * If a lock is already held, waits for it to be released before acquiring. */ async withLock(key, fn) { const existingLock = this.locks.get(key); if (existingLock) { await existingLock; } let releaseLock; const lockPromise = new Promise((resolve) => { releaseLock = resolve; }); this.locks.set(key, lockPromise); try { return await fn(); } finally { releaseLock(); if (this.locks.get(key) === lockPromise) { this.locks.delete(key); } } } constructor(config) { if (!features.coreFeatures.has("request-response-id-rotation")) { throw new Error( "Observational memory requires @mastra/core support for request-response-id-rotation. Please bump @mastra/core to a newer version." ); } if (config.model && config.observation?.model) { throw new Error( "Cannot set both `model` and `observation.model`. Use `model` to set both agents, or set each individually." ); } if (config.model && config.reflection?.model) { throw new Error( "Cannot set both `model` and `reflection.model`. Use `model` to set both agents, or set each individually." ); } this.shouldObscureThreadIds = config.obscureThreadIds || false; this.storage = config.storage; this.scope = config.scope ?? "thread"; this.retrieval = Boolean(config.retrieval); this.onIndexObservations = config.onIndexObservations; this.mastra = config.mastra; const resolveModel = (model, defaultModel) => model === "default" ? defaultModel : model; const observationModel = resolveModel(config.model, chunkD4J4XPGM_cjs.OBSERVATIONAL_MEMORY_DEFAULTS.observation.model) ?? resolveModel(config.observation?.model, chunkD4J4XPGM_cjs.OBSERVATIONAL_MEMORY_DEFAULTS.observation.model) ?? resolveModel(config.reflection?.model, chunkD4J4XPGM_cjs.OBSERVATIONAL_MEMORY_DEFAULTS.observation.model) ?? chunkD4J4XPGM_cjs.OBSERVATIONAL_MEMORY_DEFAULTS.observation.model; const reflectionModel = resolveModel(config.model, chunkD4J4XPGM_cjs.OBSERVATIONAL_MEMORY_DEFAULTS.reflection.model) ?? resolveModel(config.reflection?.model, chunkD4J4XPGM_cjs.OBSERVATIONAL_MEMORY_DEFAULTS.reflection.model) ?? resolveModel(config.observation?.model, chunkD4J4XPGM_cjs.OBSERVATIONAL_MEMORY_DEFAULTS.reflection.model) ?? chunkD4J4XPGM_cjs.OBSERVATIONAL_MEMORY_DEFAULTS.reflection.model; const messageTokens = config.observation?.messageTokens ?? chunkD4J4XPGM_cjs.OBSERVATIONAL_MEMORY_DEFAULTS.observation.messageTokens; const observationTokens = config.reflection?.observationTokens ?? chunkD4J4XPGM_cjs.OBSERVATIONAL_MEMORY_DEFAULTS.reflection.observationTokens; const isSharedBudget = config.shareTokenBudget ?? false; const isDefaultModelSelection = (model) => model === void 0 || model === "default" || model instanceof ModelByInputTokens; const observationSelectedModel = config.model ?? config.observation?.model ?? config.reflection?.model; const reflectionSelectedModel = config.model ?? config.reflection?.model ?? config.observation?.model; const observationDefaultMaxOutputTokens = config.observation?.modelSettings?.maxOutputTokens ?? (isDefaultModelSelection(observationSelectedModel) ? chunkD4J4XPGM_cjs.OBSERVATIONAL_MEMORY_DEFAULTS.observation.modelSettings.maxOutputTokens : void 0); const reflectionDefaultMaxOutputTokens = config.reflection?.modelSettings?.maxOutputTokens ?? (isDefaultModelSelection(reflectionSelectedModel) ? chunkD4J4XPGM_cjs.OBSERVATIONAL_MEMORY_DEFAULTS.reflection.modelSettings.maxOutputTokens : void 0); const totalBudget = messageTokens + observationTokens; const userExplicitlyConfiguredAsync = config.observation?.bufferTokens !== void 0 || config.observation?.bufferActivation !== void 0 || config.reflection?.bufferActivation !== void 0; const asyncBufferingDisabled = config.observation?.bufferTokens === false || config.scope === "resource" && !userExplicitlyConfiguredAsync; if (isSharedBudget && !asyncBufferingDisabled) { const common = `shareTokenBudget requires async buffering to be disabled (this is a temporary limitation). Add observation: { bufferTokens: false } to your config: observationalMemory: { shareTokenBudget: true, observation: { bufferTokens: false }, } `; if (userExplicitlyConfiguredAsync) { throw new Error( common + ` Remove any other async buffering settings (bufferTokens, bufferActivation, blockAfter).` ); } else { throw new Error( common + ` Async buffering is enabled by default \u2014 this opt-out is only needed when using shareTokenBudget.` ); } } const observationActivateAfterIdle = config.observation?.activateAfterIdle ?? config.activateAfterIdle; const observationActivateAfterIdlePath = config.observation?.activateAfterIdle !== void 0 ? "observation.activateAfterIdle" : "activateAfterIdle"; this.observationConfig = { model: observationModel, // When shared budget, store as range: min = base threshold, max = total budget // This allows messages to expand into unused observation space messageTokens: isSharedBudget ? { min: messageTokens, max: totalBudget } : messageTokens, shareTokenBudget: isSharedBudget, modelSettings: { temperature: config.observation?.modelSettings?.temperature ?? chunkD4J4XPGM_cjs.OBSERVATIONAL_MEMORY_DEFAULTS.observation.modelSettings.temperature, ...observationDefaultMaxOutputTokens !== void 0 ? { maxOutputTokens: observationDefaultMaxOutputTokens } : {} }, providerOptions: config.observation?.providerOptions ?? chunkD4J4XPGM_cjs.OBSERVATIONAL_MEMORY_DEFAULTS.observation.providerOptions, maxTokensPerBatch: config.observation?.maxTokensPerBatch ?? chunkD4J4XPGM_cjs.OBSERVATIONAL_MEMORY_DEFAULTS.observation.maxTokensPerBatch, bufferTokens: asyncBufferingDisabled ? void 0 : resolveBufferTokens( config.observation?.bufferTokens ?? chunkD4J4XPGM_cjs.OBSERVATIONAL_MEMORY_DEFAULTS.observation.bufferTokens, config.observation?.messageTokens ?? chunkD4J4XPGM_cjs.OBSERVATIONAL_MEMORY_DEFAULTS.observation.messageTokens ), bufferOnIdle: config.observation?.bufferOnIdle ?? false, bufferActivation: asyncBufferingDisabled ? void 0 : config.observation?.bufferActivation ?? chunkD4J4XPGM_cjs.OBSERVATIONAL_MEMORY_DEFAULTS.observation.bufferActivation, activateAfterIdle: parseActivationTTL(observationActivateAfterIdle, observationActivateAfterIdlePath), activateOnProviderChange: config.observation?.activateOnProviderChange ?? config.activateOnProviderChange ?? false, blockAfter: asyncBufferingDisabled ? void 0 : resolveBlockAfter( config.observation?.blockAfter ?? (config.observation?.bufferTokens ?? chunkD4J4XPGM_cjs.OBSERVATIONAL_MEMORY_DEFAULTS.observation.bufferTokens ? 1.2 : void 0), config.observation?.messageTokens ?? chunkD4J4XPGM_cjs.OBSERVATIONAL_MEMORY_DEFAULTS.observation.messageTokens ), previousObserverTokens: config.observation?.previousObserverTokens ?? 2e3, instruction: config.observation?.instruction, threadTitle: config.observation?.threadTitle ?? false, observeAttachments: config.observation?.observeAttachments ?? true }; this.reflectionConfig = { model: reflectionModel, observationTokens, shareTokenBudget: isSharedBudget, modelSettings: { temperature: config.reflection?.modelSettings?.temperature ?? chunkD4J4XPGM_cjs.OBSERVATIONAL_MEMORY_DEFAULTS.reflection.modelSettings.temperature, ...reflectionDefaultMaxOutputTokens !== void 0 ? { maxOutputTokens: reflectionDefaultMaxOutputTokens } : {} }, providerOptions: config.reflection?.providerOptions ?? chunkD4J4XPGM_cjs.OBSERVATIONAL_MEMORY_DEFAULTS.reflection.providerOptions, bufferActivation: asyncBufferingDisabled ? void 0 : config?.reflection?.bufferActivation ?? chunkD4J4XPGM_cjs.OBSERVATIONAL_MEMORY_DEFAULTS.reflection.bufferActivation, activateAfterIdle: parseActivationTTL(config.reflection?.activateAfterIdle, "reflection.activateAfterIdle"), activateOnProviderChange: config.reflection?.activateOnProviderChange ?? false, blockAfter: asyncBufferingDisabled ? void 0 : resolveBlockAfter( config.reflection?.blockAfter ?? (config.reflection?.bufferActivation ?? chunkD4J4XPGM_cjs.OBSERVATIONAL_MEMORY_DEFAULTS.reflection.bufferActivation ? 1.2 : void 0), config.reflection?.observationTokens ?? chunkD4J4XPGM_cjs.OBSERVATIONAL_MEMORY_DEFAULTS.reflection.observationTokens ), instruction: config.reflection?.instruction }; this.tokenCounter = new TokenCounter({ model: typeof observationModel === "string" ? observationModel : void 0 }); this.onDebugEvent = config.onDebugEvent; this.messageHistory = new processors.MessageHistory({ storage: this.storage }); this.observer = new ObserverRunner({ observationConfig: this.observationConfig, observedMessageIds: this.observedMessageIds, resolveModel: (inputTokens) => this.resolveObservationModel(inputTokens), tokenCounter: this.tokenCounter, mastra: config.mastra }); this.buffering = new BufferingCoordinator({ observationConfig: this.observationConfig, reflectionConfig: this.reflectionConfig, scope: this.scope }); this.reflector = new ReflectorRunner({ reflectionConfig: this.reflectionConfig, observationConfig: this.observationConfig, tokenCounter: this.tokenCounter, storage: this.storage, scope: this.scope, buffering: this.buffering, emitDebugEvent: (e) => this.emitDebugEvent(e), persistMarkerToStorage: (m, t, r) => this.persistMarkerToStorage(m, t, r), persistMarkerToMessage: (m, ml, t, r) => this.persistMarkerToMessage(m, ml, t, r), getCompressionStartLevel: (rc) => this.getCompressionStartLevel(rc), resolveModel: (inputTokens) => this.resolveReflectionModel(inputTokens), mastra: config.mastra }); this.validateBufferConfig(); omDebug( `[OM:init] new ObservationalMemory instance created \u2014 scope=${this.scope}, messageTokens=${JSON.stringify(this.observationConfig.messageTokens)}, obsAsyncEnabled=${this.buffering.isAsyncObservationEnabled()}, bufferTokens=${this.observationConfig.bufferTokens}, bufferActivation=${this.observationConfig.bufferActivation}, blockAfter=${this.observationConfig.blockAfter}, reflectionTokens=${this.reflectionConfig.observationTokens}, refAsyncEnabled=${this.buffering.isAsyncReflectionEnabled()}, refAsyncActivation=${this.reflectionConfig.bufferActivation}, refBlockAfter=${this.reflectionConfig.blockAfter}` ); } __registerMastra(mastra) { this.mastra = mastra; this.observer.__registerMastra(mastra); this.reflector.__registerMastra(mastra); } /** * Get the current configuration for this OM instance. * Used by the server to expose config to the UI when OM is added via processors. */ get config() { return { scope: this.scope, retrieval: this.retrieval, observation: { messageTokens: this.observationConfig.messageTokens, previousObserverTokens: this.observationConfig.previousObserverTokens }, reflection: { observationTokens: this.reflectionConfig.observationTokens } }; } /** * Wait for any in-flight async buffering operations for the given thread/resource. * Used by server endpoints to block until buffering completes so the UI can get final state. */ async waitForBuffering(threadId, resourceId, timeoutMs = 3e4) { return BufferingCoordinator.awaitBuffering(threadId, resourceId, this.scope, timeoutMs); } getConcreteModel(model, inputTokens) { if (model instanceof ModelByInputTokens) { if (inputTokens === void 0) { throw new Error("ModelByInputTokens requires inputTokens for resolution"); } return model.resolve(inputTokens); } return model; } getModelToResolve(model, inputTokens) { const concreteModel = this.getConcreteModel(model, inputTokens); if (Array.isArray(concreteModel)) { return concreteModel[0]?.model ?? "unknown"; } if (typeof concreteModel === "function") { return async (ctx) => { const result = await concreteModel(ctx); if (Array.isArray(result)) { return result[0]?.model ?? "unknown"; } return result; }; } return concreteModel; } formatModelName(model) { if (!model.modelId) { return "(unknown)"; } return model.provider ? `${model.provider}/${model.modelId}` : model.modelId; } resolveObservationModel(inputTokens) { return this.resolveTieredModel(this.observationConfig.model, inputTokens); } resolveReflectionModel(inputTokens) { return this.resolveTieredModel(this.reflectionConfig.model, inputTokens); } resolveTieredModel(model, inputTokens) { if (!(model instanceof ModelByInputTokens)) { return { model }; } const thresholds = model.getThresholds(); const selectedThreshold = thresholds.find((upTo) => inputTokens <= upTo) ?? thresholds.at(-1); return { model: model.resolve(inputTokens), selectedThreshold, routingStrategy: "model-by-input-tokens", routingThresholds: thresholds.join(",") }; } async resolveModelRouting(modelConfig, requestContext) { try { if (modelConfig instanceof ModelByInputTokens) { const routing = await Promise.all( modelConfig.getThresholds().map(async (upTo) => { const resolvedModel = modelConfig.resolve(upTo); const resolved2 = await this.resolveModelContext(resolvedModel, requestContext); return { upTo, model: resolved2?.modelId ? this.formatModelName(resolved2) : "(unknown)" }; }) ); return { model: routing[0]?.model ?? "(unknown)", routing }; } const resolved = await this.resolveModelContext(modelConfig, requestContext); return { model: resolved?.modelId ? this.formatModelName(resolved) : "(unknown)" }; } catch (error) { omError("[OM] Failed to resolve model config", error); return { model: "(unknown)" }; } } async resolveModelContext(modelConfig, requestContext, inputTokens) { const modelToResolve = this.getModelToResolve(modelConfig, inputTokens); if (!modelToResolve) { return void 0; } const resolved = await llm.resolveModelConfig(modelToResolve, requestContext, this.mastra); return { provider: resolved.provider, modelId: resolved.modelId }; } /** * Get the default compression start level based on model behavior. * gemini-2.5-flash is a faithful transcriber that needs explicit pressure to compress effectively. */ async getCompressionStartLevel(requestContext) { try { const resolved = await this.resolveModelContext(this.reflectionConfig.model, requestContext); const modelId = resolved?.modelId ?? ""; if (modelId.includes("gemini-2.5-flash")) { return 2; } return 1; } catch { return 1; } } /** * Get the full config including resolved model names. * This is async because it needs to resolve the model configs. */ async getResolvedConfig(requestContext) { const [observationResolved, reflectionResolved] = await Promise.all([ this.resolveModelRouting(this.observationConfig.model, requestContext), this.resolveModelRouting(this.reflectionConfig.model, requestContext) ]); return { scope: this.scope, observation: { messageTokens: this.observationConfig.messageTokens, model: observationResolved.model, previousObserverTokens: this.observationConfig.previousObserverTokens, routing: observationResolved.routing }, reflection: { observationTokens: this.reflectionConfig.observationTokens, model: reflectionResolved.model, routing: reflectionResolved.routing } }; } /** * Emit a debug event if the callback is configured. * @internal Used by observation strategies. Do not call directly. */ emitDebugEvent(event) { if (this.onDebugEvent) { this.onDebugEvent(event); } } /** * Validate buffer configuration on first use. * Ensures bufferTokens is less than the threshold and bufferActivation is valid. */ validateBufferConfig() { const hasAsyncBuffering = this.observationConfig.bufferTokens !== void 0 || this.observationConfig.bufferActivation !== void 0 || this.reflectionConfig.bufferActivation !== void 0; if (hasAsyncBuffering && this.scope === "resource") { throw new Error( `Async buffering is not yet supported with scope: 'resource'. Use scope: 'thread', or set observation: { bufferTokens: false } to disable async buffering.` ); } const observationThreshold = getMaxThreshold(this.observationConfig.messageTokens); if (this.observationConfig.bufferTokens !== void 0) { if (this.observationConfig.bufferTokens <= 0) { throw new Error(`observation.bufferTokens must be > 0, got ${this.observationConfig.bufferTokens}`); } if (this.observationConfig.bufferTokens >= observationThreshold) { throw new Error( `observation.bufferTokens (${this.observationConfig.bufferTokens}) must be less than messageTokens (${observationThreshold})` ); } } if (this.observationConfig.bufferActivation !== void 0) { if (this.observationConfig.bufferActivation <= 0) { throw new Error(`observation.bufferActivation must be > 0, got ${this.observationConfig.bufferActivation}`); } if (this.observationConfig.bufferActivation > 1 && this.observationConfig.bufferActivation < 1e3) { throw new Error( `observation.bufferActivation must be <= 1 (ratio) or >= 1000 (absolute token retention), got ${this.observationConfig.bufferActivation}` ); } if (this.observationConfig.bufferActivation >= 1e3 && this.observationConfig.bufferActivation >= observationThreshold) { throw new Error( `observation.bufferActivation as absolute retention (${this.observationConfig.bufferActivation}) must be less than messageTokens (${observationThreshold})` ); } } if (this.observationConfig.blockAfter !== void 0) { if (this.observationConfig.blockAfter < observationThreshold) { throw new Error( `observation.blockAfter (${this.observationConfig.blockAfter}) must be >= messageTokens (${observationThreshold})` ); } if (!this.observationConfig.bufferTokens) { throw new Error( `observation.blockAfter requires observation.bufferTokens to be set (blockAfter only applies when async buffering is enabled)` ); } } if (this.observationConfig.previousObserverTokens !== void 0 && this.observationConfig.previousObserverTokens !== false) { if (!Number.isFinite(this.observationConfig.previousObserverTokens) || this.observationConfig.previousObserverTokens < 0) { throw new Error( `observation.previousObserverTokens must be false or a finite number >= 0, got ${this.observationConfig.previousObserverTokens}` ); } } if (this.reflectionConfig.bufferActivation !== void 0) { if (this.reflectionConfig.bufferActivation <= 0 || this.reflectionConfig.bufferActivation > 1) { throw new Error( `reflection.bufferActivation must be in range (0, 1], got ${this.reflectionConfig.bufferActivation}` ); } } if (this.reflectionConfig.blockAfter !== void 0) { const reflectionThreshold = getMaxThreshold(this.reflectionConfig.observationTokens); if (this.reflectionConfig.blockAfter < reflectionThreshold) { throw new Error( `reflection.blockAfter (${this.reflectionConfig.blockAfter}) must be >= reflection.observationTokens (${reflectionThreshold})` ); } if (!this.reflectionConfig.bufferActivation) { throw new Error( `reflection.blockAfter requires reflection.bufferActivation to be set (blockAfter only applies when async reflection is enabled)` ); } } } /** * Resolve the effective messageTokens for a record. * Only explicit per-record overrides (stored under `_overrides`) win; * the initial config snapshot written by getOrCreateRecord() is ignored * so that later instance-level changes still take effect. * * Overrides that fall below the instance-level buffering floor * (bufferTokens / absolute bufferActivation) are clamped to the * instance threshold to preserve buffering invariants. */ getEffectiveMessageTokens(record) { const overrides = record.config?._overrides; const recordTokens = overrides?.observation?.messageTokens; if (recordTokens) { const maxOverride = getMaxThreshold(recordTokens); const bufferTokens = this.observationConfig.bufferTokens; if (bufferTokens && maxOverride <= bufferTokens) { return this.observationConfig.messageTokens; } const bufferActivation = this.observationConfig.bufferActivation; if (bufferActivation && bufferActivation >= 1e3 && maxOverride <= bufferActivation) { return this.observationConfig.messageTokens; } return recordTokens; } return this.observationConfig.messageTokens; } /** * Resolve the effective reflection observationTokens for a record. * Only explicit per-record overrides (stored under `_overrides`) win; * the initial config snapshot is ignored so instance-level changes * still take effect for existing records. */ getEffectiveReflectionTokens(record) { const overrides = record.config?._overrides; const recordTokens = overrides?.reflection?.observationTokens; if (recordTokens) { return recordTokens; } return this.reflectionConfig.observationTokens; } /** * Check whether the unobserved message tokens meet the observation threshold. */ meetsObservationThreshold(opts) { const { record, unobservedTokens, extraTokens = 0 } = opts; const pendingTokens = (record.pendingMessageTokens ?? 0) + unobservedTokens + extraTokens; const currentObservationTokens = record.observationTokenCount ?? 0; const threshold = calculateDynamicThreshold(this.getEffectiveMessageTokens(record), currentObservationTokens); return pendingTokens >= threshold; } /** * Get thread/resource IDs for storage lookup */ getStorageIds(threadId, resourceId) { if (this.scope === "resource") { return { threadId: null, resourceId: resourceId ?? threadId }; } if (!threadId) { throw new Error( `ObservationalMemory (scope: 'thread') requires a threadId, but received an empty value. This is a bug \u2014 getThreadContext should have caught this earlier.` ); } return { threadId, resourceId: resourceId ?? threadId }; } /** * Get or create the observational memory record. * Returns the existing record if one exists, otherwise initializes a new one. */ async getOrCreateRecord(threadId, resourceId) { const ids = this.getStorageIds(threadId, resourceId); let record = await this.storage.getObservationalMemory(ids.threadId, ids.resourceId); if (!record) { const observedTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone; record = await this.storage.initializeObservationalMemory({ threadId: ids.threadId, resourceId: ids.resourceId, scope: this.scope, config: { observation: this.observationConfig, reflection: this.reflectionConfig, scope: this.scope }, observedTimezone }); } return record; } // ════════════════════════════════════════════════════════════════════════════ // DATA-OM-OBSERVATION PART HELPERS (Start/End/Failed markers) // These helpers manage the observation boundary markers within messages. // // Flow: // 1. Before observation: [...messageParts] // 2. Insert start: [...messageParts, start] → stream to UI (loading state) // 3. After success: [...messageParts, start, end] → stream to UI (complete) // 4. After failure: [...messageParts, start, failed] // // For filtering, we look for the last completed observation (start + end pair). // A start without end means observation is in progress. // ════════════════════════════════════════════════════════════════════════════ /** * Get current config snapshot for observation markers. */ getObservationMarkerConfig() { return { messageTokens: getMaxThreshold(this.observationConfig.messageTokens), observationTokens: getMaxThreshold(this.reflectionConfig.observationTokens), scope: this.scope, activateAfterIdle: this.observationConfig.activateAfterIdle }; } /** * Persist a data-om-* marker part on the last assistant message in messageList * AND save the updated message to the DB so it survives page reload. * (data-* parts are filtered out before sending to the LLM, so they don't affect model calls.) * @internal Used by ReflectorRunner. Do not call directly. */ async persistMarkerToMessage(marker, messageList, threadId, resourceId) { if (!messageList) return; const allMsgs = messageList.get.all.db(); for (let i = allMsgs.length - 1; i >= 0; i--) { const msg = allMsgs[i]; if (msg?.role === "assistant" && msg.content?.parts && Array.isArray(msg.content.parts)) { const markerData = marker.data; const alreadyPresent = markerData?.cycleId && msg.content.parts.some((p) => p?.type === marker.type && p?.data?.cycleId === markerData.cycleId); if (!alreadyPresent) { msg.content.parts.push(marker); } try { await this.messageHistory.persistMessages({ messages: [msg], threadId, resourceId }); } catch (e) { omDebug(`[OM:persistMarker] failed to save marker to DB: ${e}`); } return; } } } /** * Persist a marker to the last assistant message in storage. * Unlike persistMarkerToMessage, this fetches messages directly from the DB * so it works even when no MessageList is available (e.g. async buffering ops). * @internal Used by observation strategies. Do not call directly. */ async persistMarkerToStorage(marker, threadId, resourceId) { try { const result = await this.storage.listMessages({ threadId, perPage: 20, orderBy: { field: "createdAt", direction: "DESC" } }); const messages = result?.messages ?? []; for (const msg of messages) { if (msg?.role === "assistant" && msg.content?.parts && Array.isArray(msg.content.parts)) { const markerData = marker.data; const alreadyPresent = markerData?.cycleId && msg.content.parts.some((p) => p?.type === marker.type && p?.data?.cycleId === markerData.cycleId); if (!alreadyPresent) { msg.content.parts.push(marker); } await this.messageHistory.persistMessages({ messages: [msg], threadId, resourceId }); return; } } } catch (e) { omDebug(`[OM:persistMarkerToStorage] failed to save marker to DB: ${e}`); } } /** * Find the last completed observation boundary in a message's parts. * A completed observation is a start marker followed by an end marker. * * Returns the index of the END marker (which is the observation boundary), * or -1 if no completed observation is found. */ /** * Check if a message has an in-progress observation (start without end). */ hasInProgressObservation(message) { const parts = message.content?.parts; if (!parts || !Array.isArray(parts)) return false; let lastStartIndex = -1; let lastEndOrFailedIndex = -1; for (let i = parts.length - 1; i >= 0; i--) { const part = parts[i]; if (part?.type === "data-om-observation-start" && lastStartIndex === -1) { lastStartIndex = i; } if ((part?.type === "data-om-observation-end" || part?.type === "data-om-observation-failed") && lastEndOrFailedIndex === -1) { lastEndOrFailedIndex = i; } } return lastStartIndex !== -1 && lastStartIndex > lastEndOrFailedIndex; } /** * Seal messages to prevent new parts from being merged into them. * This is used when starting buffering to capture the current content state. * * Sealing works by: * 1. Setting `message.content.metadata.mastra.sealed = true` (message-level flag) * 2. Adding `metadata.mastra.sealedAt` to the last part (boundary marker) * * When MessageList.add() receives a message with the same ID as a sealed message, * it creates a new message with only the parts beyond the seal boundary. * * The messages are mutated in place - since they're references to the same objects * in the MessageList, the seal will be recognized immediately. * * @param messages - Messages to seal (mutated in place) */ /** @internal Used by ObservationStep. */ sealMessagesForBuffering(messages) { const sealedAt = Date.now(); for (const msg of messages) { if (!msg.content?.parts?.length) continue; if (!msg.content.metadata) { msg.content.metadata = {}; } const metadata = msg.content.metadata; if (!metadata.mastra) { metadata.mastra = {}; } metadata.mastra.sealed = true; const lastPart = msg.content.parts[msg.content.parts.length - 1]; if (!lastPart.metadata) { lastPart.metadata = {}; } if (!lastPart.metadata.mastra) { lastPart.metadata.mastra = {}; } lastPart.metadata.mastra.sealedAt = sealedAt; } } /** * Insert an observation marker into a message. * The marker is appended directly to the message's parts array (mutating in place). * Also persists the change to storage so markers survive page refresh. * * For end/failed markers, the message is also "sealed" to prevent future content * from being merged into it. This ensures observation markers are preserved. */ /** * Insert an observation marker into a message. * For start markers, this pushes the part directly. * For end/failed markers, this should be called AFTER writer.custom() has added the part, * so we just find the part and add sealing metadata. */ /** * Create a virtual message containing only the unobserved parts. * This is used for token counting and observation. */ createUnobservedMessage(message) { const unobservedParts = getUnobservedParts(message); if (unobservedParts.length === 0) return null; return { ...message, content: { ...message.content, parts: unobservedParts } }; } /** * Get unobserved messages with part-level filtering. * * This method uses data-om-observation-end markers to filter at the part level: * 1. For messages WITH a completed observation: only return parts AFTER the end marker * 2. For messages WITHOUT completed observation: check timestamp against lastObservedAt * * This handles the case where a single message accumulates many parts * (like tool calls) during an agentic loop - we only observe the new parts. */ /** @internal Used by ObservationStep. */ getUnobservedMessages(allMessages, record, opts) { const lastObservedAt = record.lastObservedAt; const observedMessageIds = new Set( Array.isArray(record.observedMessageIds) ? record.observedMessageIds : [] ); if (opts?.excludeBuffered) { const bufferedChunks = getBufferedChunks(record); for (const chunk of bufferedChunks) { if (Array.isArray(chunk.messageIds)) { for (const id of chunk.messageIds) { observedMessageIds.add(id); } } } } const result = []; for (const msg of allMessages) { if (msg.role === "system") { continue; } if (observedMessageIds?.has(msg.id)) { continue; } const endMarkerIndex = findLastCompletedObservationBoundary(msg); const inProgress = this.hasInProgressObservation(msg); if (inProgress) { result.push(msg); } else if (endMarkerIndex !== -1) { const virtualMsg = this.createUnobservedMessage(msg); if (virtualMsg) { result.push(virtualMsg); } } else { if (!msg.createdAt || !lastObservedAt) { result.push(msg); } else { const msgDate = new Date(msg.createdAt); if (msgDate > lastObservedAt) { result.push(msg); } } } } return result; } /** * Prepare optimized observer context by applying truncation and buffered-reflection inclusion. * * Returns the (possibly optimized) observations string to pass as "Previous Observations" * to the observer prompt. When no optimization options are set, returns the input unchanged. */ prepareObserverContext(existingObservations, record) { const { previousObserverTokens } = this.observationConfig; const tokenBudget = previousObserverTokens === void 0 || previousObserverTokens === false ? void 0 : previousObserverTokens; if (tokenBudget === void 0) { return { context: existingObservations, wasTruncated: false }; } const bufferedReflection = record?.bufferedReflection && record?.reflectedObservationLineCount ? record.bufferedReflection : void 0; if (!existingObservations) { return { context: bufferedReflection, wasTruncated: false }; } let observations = existingObservations; if (bufferedReflection && record?.reflectedObservationLineCount) { const allLines = observations.split("\n"); const unreflectedLines = allLines.slice(record.reflectedObservationLineCount); const unreflectedContent = unreflectedLines.join("\n").trim(); observations = unreflectedContent ? `${bufferedReflection} ${unreflectedContent}` : bufferedReflection; } let wasTruncated = false; if (tokenBudget !== void 0) { if (tokenBudget === 0) { return { context: "", wasTruncated: true }; } const currentTokens = this.tokenCounter.countObservations(observations); if (currentTokens > tokenBudget) { observations = this.truncateObservationsToTokenBudget(observations, tokenBudget); wasTruncated = true; } } return { context: observations, wasTruncated }; } /** * Truncate observations to fit within a token budget. * * Strategy: * 1. Keep a raw tail of recent observations (end of block). * 2. Add a truncation marker: [X observations truncated here], placed at the hidden gap. * 3. Try to preserve important observations (🔴) from older context, newest-first. * 4. Enforce that at least 50% of kept observations remain raw tail observations. */ truncateObservationsToTokenBudget(observations, budget) { if (budget === 0) { return ""; } const totalTokens = this.tokenCounter.countObservations(observations); if (totalTokens <= budget) { return observations; } const lines = observations.split("\n"); const totalCount = lines.length; const lineTokens = new Array(totalCount); const isImportant = new Array(totalCount); for (let i = 0; i < totalCount; i++) { lineTokens[i] = this.tokenCounter.countString(lines[i]); isImportant[i] = lines[i].includes("\u{1F534}") || lines[i].includes("\u2705"); } const suffixTokens = new Array(totalCount + 1); suffixTokens[totalCount] = 0; for (let i = totalCount - 1; i >= 0; i--) { suffixTokens[i] = suffixTokens[i + 1] + lineTokens[i]; } const headImportantIndexes = []; const buildCandidateString = (tailStart, selectedImportantIndexes) => { const keptIndexes = [ ...selectedImportantIndexes, ...Array.from({ length: totalCount - tailStart }, (_, i) => tailStart + i) ].sort((a, b) => a - b); if (keptIndexes.length === 0) { return `[${totalCount} observations truncated here]`; } const outputLines = []; let previousKeptIndex = -1; for (const keptIndex of keptIndexes) { const hiddenCount = keptIndex - previousKeptIndex - 1; if (hiddenCount === 1) { outputLines.push(lines[previousKeptIndex + 1]); } else if (hiddenCount > 1) { outputLines.push(`[${hiddenCount} observations truncated here]`); } outputLines.push(lines[keptIndex]); previousKeptIndex = keptIndex; } const trailingHiddenCount = totalCount - previousKeptIndex - 1; if (trailingHiddenCount === 1) { outputLines.push(lines[totalCount - 1]); } else if (trailingHiddenCount > 1) { outputLines.push(`[${trailingHiddenCount} observations truncated here]`); } return outputLines.join("\n"); }; const estimateKeptContentCost = (tailStart, selectedImportantIndexes) => { let cost = suffixTokens[tailStart]; for (const idx of selectedImportantIndexes) { cost += lineTokens[idx]; } return cost; }; let bestCandidate; let bestImportantCount = -1; let bestRawTailLength = -1; for (let tailStart = 1; tailStart < totalCount; tailStart++) { if (isImportant[tailStart - 1]) { headImportantIndexes.push(tailStart - 1); } const rawTailLength = totalCount - tailStart; const maxImportantByRatio = rawTailLength; let importantToKeep = Math.min(headImportantIndexes.length, maxImportantByRatio); const getSelectedImportant = (count) => count > 0 ? headImportantIndexes.slice(Math.max(0, headImportantIndexes.length - count)) : []; while (importantToKeep > 0 && estimateKeptContentCost(tailStart, getSelectedImportant(importantToKeep)) > budget) { importantToKeep -= 1; } if (estimateKeptContentCost(tailStart, getSelectedImportant(importantToKeep)) > budget) { continue; } if (importantToKeep > bestImportantCount || importantToKeep === bestImportantCount && rawTailLength > bestRawTailLength) { const candidate = buildCandidateString(tailStart, getSelectedImportant(importantToKeep)); if (this.tokenCounter.countObservations(candidate) <= budget) { bestCandidate = candidate; bestImportantCount = importantToKeep; bestRawTailLength = rawTailLength; } } } if (!bestCandidate) { return `[${totalCount} observations truncated here]`; } return bestCandidate; } /** * Format observations for injection into context. * Applies token optimization before presenting to the Actor. * * In resource scope mode, filters continuity messages to only show * the message for the current thread. */ formatObservationsForContext(observations, currentTask, suggestedResponse, unobservedContextBlocks, currentDate, retrieval = false) { let optimized = retrieval ? renderObservationGroupsForReflection(observations) ?? optimizeObservationsForContext(observations) : optimizeObservationsForContext(observations); if (currentDate) { optimized = addRelativeTimeToObservations(optimized, currentDate); } const messages = [ `${chunkD4J4XPGM_cjs.OBSERVATION_CONTEXT_PROMPT} ${chunkD4J4XPGM_cjs.OBSERVATION_CONTEXT_INSTRUCTIONS}${retrieval ? ` ${chunkD4J4XPGM_cjs.OBSERVATION_RETRIEVAL_INSTRUCTIONS}` : ""}` ]; if (unobservedContextBlocks) { messages.push( `The following content is from OTHER conversations different from the current conversation, they're here for reference, but they're not necessarily your focus: START_OTHER_CONVERSATIONS_BLOCK ${unobservedContextBlocks} END_OTHER_CONVERSATIONS_BLOCK` ); } const observationChunks = this.splitObservationContextChunks(optimized); if (observationChunks.length > 0) { messages.push("", ...observationChunks); } if (currentTask) { messages.push(` ${currentTask} `); } if (suggestedResponse) { messages.push(` ${suggestedResponse} `); } return messages; } splitObservationContextChunks(observations) { const trimmed = observations.trim(); if (!trimmed) { return []; } return trimmed.split(/\n{2,}--- message boundary \(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z\) ---\n{2,}/).map((chunk) => chunk.trim()).filter(Boolean); } /** * Create a message boundary delimiter with an ISO 8601 date. * The date should be the lastObservedAt timestamp — the latest message * timestamp that was observed to produce the observations following this boundary. */ static createMessageBoundary(date) { return ` --- message boundary (${date.toISOString()}) --- `; } /** * Get threadId and resourceId from either RequestContext or MessageList */ getThreadContext(requestContext, messageList) { const memoryContext = requestContext?.get("MastraMemory"); if (memoryContext?.thread?.id) { return { threadId: memoryContext.thread.id, resourceId: memoryContext.resourceId }; } const serialized = messageList.serialize(); if (serialized.memoryInfo?.threadId) { return { threadId: serialized.memoryInfo.threadId, resourceId: serialized.memoryInfo.resourceId }; } if (this.scope === "thread") { throw new Error( `ObservationalMemory (scope: 'thread') requires a threadId, but none was found in RequestContext or MessageList. Ensure the agent is configured with Memory and a valid threadId is provided.` ); } return null; } /** * Save messages to storage, skipping messages that were already persisted by * async buffering. Uses the message-level sealed flag (metadata.mastra.sealed) * to detect already-persisted messages, avoiding redundant DB operations. * * Messages with observation markers are always saved (upserted) even if sealed, * because the markers need to be persisted to storage. */ async persistMessages(messagesToSave, threadId, resourceId) { const filteredMessages = []; for (const msg of messagesToSave) { const isSealed = !!msg.content?.metadata?.mastra?.sealed; if (isSealed) { if (findLastCompletedObservationBoundary(msg) !== -1) { filteredMessages.push(msg); } } else { filteredMessages.push(msg); } } if (filteredMessages.length > 0) { await this.messageHistory.persistMessages({ messages: filteredMessages, threadId, resourceId }); } } /** * Load messages from storage that haven't been observed yet. * Uses cursor-based query with lastObservedAt timestamp for efficiency. * * In resource scope mode, loads messages for the entire resource (all threads). * In thread scope mode, loads messages for just the current thread. */ async loadMessagesFromStorage(threadId, resourceId, lastObservedAt) { const startDate = lastObservedAt ? new Date(lastObservedAt.getTime() + 1) : void 0; let result; if (this.scope === "resource" && resourceId) { result = await this.storage.listMessagesByResourceId({ resourceId, perPage: false, // Get all messages (no pagination limit) orderBy: { field: "createdAt", direction: "ASC" }, filter: startDate ? { dateRange: { start: startDate } } : void 0 }); } else { result = await this.storage.listMessages({ threadId, perPage: false, // Get all messages (no pagination limit) orderBy: { field: "createdAt", direction: "ASC" }, filter: startDate ? { dateRange: { start: startDate } } : void 0 }); } return result.messages.filter((msg) => msg.role !== "system"); } /** * Format unobserved messages from other threads as blocks. * These are injected into the Actor's context so it has awareness of activity * in other threads for the same resource. */ async formatUnobservedContextBlocks(messagesByThread, currentThreadId) { const blocks = []; for (const [threadId, messages] of messagesByThread) { if (threadId === currentThreadId) continue; if (messages.length === 0) continue; const formattedMessages = formatMessagesForObserver(messages, { maxPartLength: 500 }); if (formattedMessages) { const obscuredId = await this.representThreadIDInContext(threadId); blocks.push(` ${formattedMessages} `); } } return blocks.join("\n\n"); } async representThreadIDInContext(threadId) { if (this.shouldObscureThreadIds) { const hasher = await this.hasher; return hasher.h32ToString(threadId); } return threadId; } /** * Get the maximum createdAt timestamp from a list of messages. * Used to set lastObservedAt to the most recent message timestamp instead of current time. * This ensures historical data (like LongMemEval fixtures) works correctly. */ getMaxMessageTimestamp(messages) { let maxTime = 0; for (const msg of messages) { if (msg.createdAt) { const msgTime = new Date(msg.createdAt).getTime(); if (msgTime > maxTime) { maxTime = msgTime; } } } return maxTime > 0 ? new Date(maxTime) : /* @__PURE__ */ new Date(); } /** * Wrap observations in a thread attribution tag. * Used in resource scope to track which thread observations came from. * @internal Used by observation strategies. Do not call directly. */ async wrapWithThreadTag(threadId, observations, messageRange) { const cleanObservations = stripThreadTags(observations); const groupedObservations = this.retrieval && messageRange ? wrapInObservationGroup(cleanObservations, messageRange) : cleanObservations; const obscuredId = await this.representThreadIDInContext(threadId); return ` ${groupedObservations} `; } /** * Append or merge new thread sections. * If the new section has the same thread ID and date as an existing section, * merge the observations into that section to reduce token usage. * Otherwise, append as a new section. */ replaceOrAppendThreadSection(existingObservations, _threadId, newThreadSection, lastObservedAt) { if (!existingObservations) { return newThreadSection; } const threadIdMatch = newThreadSection.match(//); const dateMatch = newThreadSection.match(/Date:\s*([A-Za-z]+\s+\d+,\s+\d+)/); if (!threadIdMatch || !dateMatch) { return `${existingObservations}${_ObservationalMemory.createMessageBoundary(lastObservedAt)}${newThreadSection}`; } const newThreadId = threadIdMatch[1]; const newDate = dateMatch[1]; const threadOpen = ``; const threadClose = ""; const startIdx = existingObservations.indexOf(threadOpen); let existingSection = null; let existingSectionStart = -1; let existingSectionEnd = -1; if (startIdx !== -1) { const closeIdx = existingObservations.indexOf(threadClose, startIdx); if (closeIdx !== -1) { existingSectionEnd = closeIdx + threadClose.length; existingSectionStart = startIdx; const section = existingObservations.slice(startIdx, existingSectionEnd); if (section.includes(`Date: ${newDate}`) || section.includes(`Date:${newDate}`)) { existingSection = section; } } } if (existingSection) { const dateLineEnd = newThreadSection.indexOf("\n", newThreadSection.indexOf("Date:")); const newCloseIdx = newThreadSection.lastIndexOf(threadClose); if (dateLineEnd !== -1 && newCloseIdx !== -1) { const newObsContent = newThreadSection.slice(dateLineEnd + 1, newCloseIdx).trim(); if (newObsContent) { const withoutClose = existingSection.slice(0, existingSection.length - threadClose.length).trimEnd(); const merged = `${withoutClose} ${newObsContent} ${threadClose}`; return existingObservations.slice(0, existingSectionStart) + merged + existingObservations.slice(existingSectionEnd); } } } return `${existingObservations}${_ObservationalMemory.createMessageBoundary(lastObservedAt)}${newThreadSection}`; } /** * @internal Used by observation strategies. Do not call directly. */ wrapObservations(rawObservations, existingObservations, threadId, lastObservedAt, messageRange) { if (this.scope === "resource") { return (async () => { const threadSection = await this.wrapWithThreadTag(threadId, rawObservations, messageRange); return this.replaceOrAppendThreadSection( existingObservations, threadId, threadSection, lastObservedAt ?? /* @__PURE__ */ new Date() ); })(); } const grouped = this.retrieval && messageRange ? wrapInObservationGroup(rawObservations, messageRange) : rawObservations; return existingObservations ? `${existingObservations} ${grouped}` : grouped; } // ──────────────────────────────────────────────────────────────────────── // Observation methods // ──────────────────────────────────────────────────────────────────────── /** * Start an async background observation that stores results to bufferedObservations. * This is a fire-and-forget operation that runs in the background. * The results will be swapped to active when the main threshold is reached. * * If another buffering operation is already in progress for this scope, this will * wait for it to complete before starting a new one (mutex behavior). * * @param record - Current OM record * @param threadId - Thread ID * @param unobservedMessages - All unobserved messages (will be filtered for already-buffered) * @param lockKey - Lock key for this scope * @param writer - Optional stream writer for emitting buffering markers */ async startAsyncBufferedObservation(record, threadId, unobservedMessages, lockKey, writer, contextWindowTokens, requestContext, observabilityContext) { const bufferKey = this.buffering.getObservationBufferKey(lockKey); const currentTokens = contextWindowTokens ?? await this.tokenCounter.countMessagesAsync(unobservedMessages) + (record.pendingMessageTokens ?? 0); BufferingCoordinator.lastBufferedBoundary.set(bufferKey, currentTokens); registerOp(record.id, "bufferingObservation"); this.storage.setBufferingObservationFlag(record.id, true, currentTokens).catch((err) => { omError("[OM] Failed to set buffering observation flag", err); }); const asyncOp = this.runAsyncBufferedObservation( record, threadId, unobservedMessages, bufferKey, writer, requestContext, observabilityContext ).finally(() => { BufferingCoordinator.asyncBufferingOps.delete(bufferKey); unregisterOp(record.id, "bufferingObservation"); this.storage.setBufferingObservationFlag(record.id, false).catch((err) => { omError("[OM] Failed to clear buffering observation flag", err); }); }); BufferingCoordinator.asyncBufferingOps.set(bufferKey, asyncOp); } /** * Internal method that waits for existing buffering operation and then runs new buffering. * This implements the mutex-wait behavior. */ async runAsyncBufferedObservation(record, threadId, unobservedMessages, bufferKey, writer, requestContext, observabilityContext) { const existingOp = BufferingCoordinator.asyncBufferingOps.get(bufferKey); if (existingOp) { try { await existingOp; } catch { } } const freshRecord = await this.storage.getObservationalMemory(record.threadId, record.resourceId); if (!freshRecord) { return; } let bufferCursor = BufferingCoordinator.lastBufferedAtTime.get(bufferKey) ?? freshRecord.lastBufferedAtTime ?? null; if (freshRecord.lastObservedAt) { const lastObserved = new Date(freshRecord.lastObservedAt); if (!bufferCursor || lastObserved > bufferCursor) { bufferCursor = lastObserved; } } let candidateMessages = this.getUnobservedMessages(unobservedMessages, freshRecord, { excludeBuffered: true }); const preFilterCount = candidateMessages.length; if (bufferCursor) { candidateMessages = candidateMessages.filter((msg) => { if (!msg.createdAt) return true; return new Date(msg.createdAt) > bufferCursor; }); } omDebug( `[OM:bufferCursor] cursor=${bufferCursor?.toISOString() ?? "null"}, unobserved=${unobservedMessages.length}, afterExcludeBuffered=${preFilterCount}, afterCursorFilter=${candidateMessages.length}` ); const bufferTokens = this.observationConfig.bufferTokens ?? 5e3; const minNewTokens = bufferTokens / 2; const newTokens = await this.tokenCounter.countMessagesAsync(candidateMessages); if (newTokens < minNewTokens) { return; } const messagesToBuffer = candidateMessages; this.sealMessagesForBuffering(messagesToBuffer); await this.messageHistory.persistMessages({ messages: messagesToBuffer, threadId, resourceId: freshRecord.resourceId ?? void 0 }); const cycleId = `buffer-obs-${Date.now()}-${Math.random().toString(36).slice(2, 11)}`; const startedAt = (/* @__PURE__ */ new Date()).toISOString(); const tokensToBuffer = await this.tokenCounter.countMessagesAsync(messagesToBuffer); const startMarker = createBufferingStartMarker({ cycleId, operationType: "observation", tokensToBuffer, recordId: freshRecord.id, threadId, threadIds: [threadId], config: this.getObservationMarkerConfig() }); await this.persistMarkerToStorage(startMarker, threadId, freshRecord.resourceId ?? void 0); if (writer) { void writer.custom({ ...startMarker, transient: true }).catch(() => { }); } omDebug( `[OM:bufferInput] cycleId=${cycleId}, msgCount=${messagesToBuffer.length}, msgTokens=${tokensToBuffer}, ids=${messagesToBuffer.map((m) => `${m.id?.slice(0, 8)}@${m.createdAt ? new Date(m.createdAt).toISOString() : "none"}`).join(",")}` ); await ObservationStrategy.create(this, { record: freshRecord, threadId, resourceId: freshRecord.resourceId ?? void 0, messages: messagesToBuffer, cycleId, startedAt, writer, requestContext, observabilityContext }).run(); const maxTs = this.getMaxMessageTimestamp(messagesToBuffer); const cursor = new Date(maxTs.getTime() + 1); BufferingCoordinator.lastBufferedAtTime.set(bufferKey, cursor); } // ════════════════════════════════════════════════════════════════════════════ // HIGH-LEVEL API — semantic operations for programmatic use // ════════════════════════════════════════════════════════════════════════════ /** * Trigger async buffered observation if the token count has crossed a new interval. * * Encapsulates the shouldTrigger check + startAsyncBufferedObservation call. * Returns whether buffering was actually triggered. */ async triggerAsyncBuffering(opts) { if (!this.buffering.isAsyncObservationEnabled()) return false; const lockKey = this.buffering.getLockKey(opts.threadId, opts.resourceId); const shouldTrigger = this.buffering.shouldTriggerAsyncObservation( opts.pendingTokens, lockKey, opts.record, this.storage, opts.threshold ); if (shouldTrigger) { void this.startAsyncBufferedObservation( opts.record, opts.threadId, opts.unobservedMessages, lockKey, opts.writer, opts.unbufferedPendingTokens, opts.requestContext ); } return shouldTrigger; } isMessageList(value) { return !!value && typeof value === "object" && "get" in value && "removeByIds" in value; } removeIdsFromArray(messages, ids) { if (ids.length === 0) return; const idsSet = new Set(ids); for (let i = messages.length - 1; i >= 0; i--) { const msg = messages[i]; if (msg?.id && idsSet.has(msg.id)) { messages.splice(i, 1); } } } /** * Mutate partially observed messages in place and return the fully observed * message IDs that should be removed from the live context. * * This is the shared activation-cleanup primitive used by both the processor * and AI SDK integrations: callers pass the current live messages, OM trims * any partially observed messages down to their unobserved parts, and OM * returns only the IDs that are safe to remove entirely. */ async getObservedMessageIdsForCleanup(opts) { const { threadId, resourceId, messages, observedMessageIds, retentionFloor } = opts; const record = await this.getOrCreateRecord(threadId, resourceId); const effectiveObservedIds = observedMessageIds && observedMessageIds.length > 0 ? observedMessageIds : Array.isArray(record.observedMessageIds) ? record.observedMessageIds : []; if (effectiveObservedIds.length === 0) { return []; } const observedSet = new Set(effectiveObservedIds); const idsToRemove = /* @__PURE__ */ new Set(); const removalOrder = []; let skipped = 0; let backoffTriggered = false; const retentionCounter = typeof retentionFloor === "number" ? new TokenCounter() : null; for (const msg of messages) { if (!msg?.id || msg.id === "om-continuation" || !observedSet.has(msg.id)) continue; const unobservedParts = getUnobservedParts(msg); const totalParts = msg.content?.parts?.length ?? 0; if (unobservedParts.length > 0 && unobservedParts.length < totalParts) { msg.content.parts = unobservedParts; continue; } if (retentionCounter && typeof retentionFloor === "number") { const nextRemainingMessages = messages.filter( (m) => m?.id && m.id !== "om-continuation" && !idsToRemove.has(m.id) && m.id !== msg.id ); const remainingIfRemoved = retentionCounter.countMessages(nextRemainingMessages); if (remainingIfRemoved < retentionFloor) { skipped += 1; backoffTriggered = true; break; } } idsToRemove.add(msg.id); removalOrder.push(msg.id); } if (retentionCounter && typeof retentionFloor === "number" && idsToRemove.size > 0) { let remainingMessages = messages.filter((m) => m?.id && m.id !== "om-continuation" && !idsToRemove.has(m.id)); let remainingTokens = retentionCounter.countMessages(remainingMessages); while (remainingTokens < retentionFloor && removalOrder.length > 0) { const restoreId = removalOrder.pop(); idsToRemove.delete(restoreId); skipped += 1; backoffTriggered = true; remainingMessages = messages.filter((m) => m?.id && m.id !== "om-continuation" && !idsToRemove.has(m.id)); remainingTokens = retentionCounter.countMessages(remainingMessages); } } omDebug( `[OM:cleanupActivation] matched=${idsToRemove.size}, skipped=${skipped}, backoffTriggered=${backoffTriggered}` ); return [...idsToRemove]; } /** * Clean up observed content from either a live MessageList or a plain message array. * * - MessageList input: mutates the live container in place and returns the remaining messages * - Array input: mutates the array in place and returns it * * This is the shared cleanup primitive intended for both processor and non-processor * integrations. The processor may still pass sealedIds/state so marker/fallback cleanup * can persist messages safely, but callers that do not need that bookkeeping can omit it. */ /** @internal Used by ObservationStep. */ async cleanupMessages(opts) { const { threadId, resourceId, observedMessageIds, retentionFloor } = opts; const messageList = this.isMessageList(opts.messages) ? opts.messages : void 0; const allMsgs = messageList ? messageList.get.all.db() : opts.messages; let markerIdx = -1; let markerMsg = null; for (let i = allMsgs.length - 1; i >= 0; i--) { const msg = allMsgs[i]; if (!msg) continue; if (findLastCompletedObservationBoundary(msg) !== -1) { markerIdx = i; markerMsg = msg; break; } } omDebug( `[OM:cleanupBranch] allMsgs=${allMsgs.length}, markerFound=${markerIdx !== -1}, markerIdx=${markerIdx}, observedMessageIds=${observedMessageIds?.length ?? "undefined"}` ); if (observedMessageIds && observedMessageIds.length > 0) { const idsToRemoveList = await this.getObservedMessageIdsForCleanup({ threadId, resourceId, messages: allMsgs, observedMessageIds, retentionFloor }); if (messageList) { if (idsToRemoveList.length > 0) { messageList.removeByIds(idsToRemoveList); } return messageList.get.all.db(); } this.removeIdsFromArray(allMsgs, idsToRemoveList); return allMsgs; } if (markerMsg && markerIdx !== -1) { const idsToRemove = []; const messagesToSave = []; for (let i = 0; i < markerIdx; i++) { const msg = allMsgs[i]; if (msg?.id && msg.id !== "om-continuation") { idsToRemove.push(msg.id); messagesToSave.push(msg); } } messagesToSave.push(markerMsg); const unobservedParts = getUnobservedParts(markerMsg); if (unobservedParts.length === 0) { if (markerMsg.id) idsToRemove.push(markerMsg.id); } else if (unobservedParts.length < (markerMsg.content?.parts?.length ?? 0)) { markerMsg.content.parts = unobservedParts; } if (messageList) { if (idsToRemove.length > 0) { messageList.removeByIds(idsToRemove); } if (messagesToSave.length > 0) { await this.persistMessages(messagesToSave, threadId, resourceId); } omDebug(`[OM:cleanupMarker] removed ${idsToRemove.length} messages, saved ${messagesToSave.length}`); return messageList.get.all.db(); } this.removeIdsFromArray(allMsgs, idsToRemove); return allMsgs; } return messageList ? messageList.get.all.db() : allMsgs; } /** * Clean up the message context after a successful observation. * * Handles both activation-based cleanup (using observedMessageIds) and * marker-based cleanup (using observation boundary markers). Respects * retention floors to prevent removing too many messages. */ async cleanupObservedContext(opts) { const { messageList, threadId, resourceId, observedMessageIds, retentionFloor } = opts; await this.cleanupMessages({ threadId, resourceId, messages: messageList, observedMessageIds, retentionFloor }); } /** * Reset buffering state after a successful observation activation. * * Clears the lastBufferedBoundary, buffering flag, and optionally cleans up * static maps for activated message IDs. */ /** @internal Used by ObservationStep. */ async resetBufferingState(opts) { const { threadId, resourceId, recordId, activatedMessageIds } = opts; const lockKey = this.buffering.getLockKey(threadId, resourceId); const bufKey = this.buffering.getObservationBufferKey(lockKey); BufferingCoordinator.lastBufferedBoundary.set(bufKey, 0); await this.storage.setBufferingObservationFlag(recordId, false, 0).catch(() => { }); if (activatedMessageIds && activatedMessageIds.length > 0) { this.buffering.cleanupStaticMaps(threadId, resourceId, activatedMessageIds); } } /** * Build the observation system message string for injection into an LLM prompt. * * Loads thread metadata (currentTask, suggestedResponse), formats observations * with context prompts and instructions, and returns the fully-formed string. * Returns undefined if no observations exist. * * This is the public entry point for context formatting — used by both * Memory.getContext() (standalone) and the processor (via injectObservationsIntoMessages). * * @example * ```ts * const systemMsg = await om.buildContextSystemMessage({ threadId: 'thread-1' }); * if (systemMsg) { * const result = await generateText({ system: systemMsg, messages }); * } * ``` */ async buildContextSystemMessage(opts) { const parts = await this.buildContextSystemMessages(opts); return parts?.join("\n\n"); } /** * Build observation context as an array of system message chunks. * Each chunk is a separate system message for better LLM cache hit rates. * Used by the processor to inject multiple system messages. * @internal */ async buildContextSystemMessages(opts) { const { threadId, resourceId, unobservedContextBlocks } = opts; const record = opts.record ?? await this.getOrCreateRecord(threadId, resourceId); if (!record.activeObservations) return void 0; const thread = await this.storage.getThreadById({ threadId }); const omMetadata = memory.getThreadOMMetadata(thread?.metadata); const currentTask = omMetadata?.currentTask; const suggestedResponse = omMetadata?.suggestedResponse; const currentDate = opts.currentDate ?? /* @__PURE__ */ new Date(); return this.formatObservationsForContext( record.activeObservations, currentTask, suggestedResponse, unobservedContextBlocks, currentDate, this.retrieval ); } /** * Get unobserved messages from other threads for resource-scoped observation. * * Lists all threads for the resource, filters to unobserved messages, * and formats them as context blocks. */ /** @internal Used by ObservationTurn. */ async getOtherThreadsContext(resourceId, currentThreadId) { const { threads: allThreads } = await this.storage.listThreads({ filter: { resourceId } }); const messagesByThread = /* @__PURE__ */ new Map(); const record = await this.getRecord(currentThreadId, resourceId); const recordLastObservedAt = record?.lastObservedAt; for (const thread of allThreads) { if (thread.id === currentThreadId) continue; const omMetadata = memory.getThreadOMMetadata(thread.metadata); const threadLastObservedAt = omMetadata?.lastObservedAt ?? recordLastObservedAt; const startDate = threadLastObservedAt ? new Date(new Date(threadLastObservedAt).getTime() + 1) : void 0; const result = await this.storage.listMessages({ threadId: thread.id, perPage: false, orderBy: { field: "createdAt", direction: "ASC" }, filter: startDate ? { dateRange: { start: startDate } } : void 0 }); const filtered = result.messages.filter((m) => !this.observedMessageIds.has(m.id)); if (filtered.length > 0) { messagesByThread.set(thread.id, filtered); } } if (messagesByThread.size === 0) return void 0; const blocks = await this.formatUnobservedContextBlocks(messagesByThread, currentThreadId); return blocks || void 0; } /** * Emit debug event and stream progress for UI feedback. */ async emitProgress(opts) { const { record, pendingTokens, threshold, effectiveObservationTokensThreshold, currentObservationTokens, writer, stepNumber, threadId, resourceId } = opts; this.emitDebugEvent({ type: "step_progress", timestamp: /* @__PURE__ */ new Date(), threadId, resourceId: resourceId ?? "", stepNumber, finishReason: "unknown", pendingTokens, threshold, thresholdPercent: Math.round(pendingTokens / threshold * 100), willSave: pendingTokens >= threshold, willObserve: pendingTokens >= threshold }); if (writer) { const bufferedChunks = getBufferedChunks(record); const bufferedObservationTokens = bufferedChunks.reduce((sum, chunk) => sum + (chunk.tokenCount ?? 0), 0); const rawBufferedMessageTokens = bufferedChunks.reduce((sum, chunk) => sum + (chunk.messageTokens ?? 0), 0); const bufferedMessageTokens = Math.min(rawBufferedMessageTokens, pendingTokens); const projectedMessageRemoval = calculateProjectedMessageRemoval( bufferedChunks, this.observationConfig.bufferActivation ?? 1, getMaxThreshold(this.getEffectiveMessageTokens(record)), pendingTokens ); let obsBufferStatus = "idle"; if (record.isBufferingObservation) obsBufferStatus = "running"; else if (bufferedChunks.length > 0) obsBufferStatus = "complete"; let refBufferStatus = "idle"; if (record.isBufferingReflection) refBufferStatus = "running"; else if (record.bufferedReflection && record.bufferedReflection.length > 0) refBufferStatus = "complete"; const statusPart = { type: "data-om-status", data: { windows: { active: { messages: { tokens: pendingTokens, threshold }, observations: { tokens: currentObservationTokens, threshold: effectiveObservationTokensThreshold } }, buffered: { observations: { chunks: bufferedChunks.length, messageTokens: bufferedMessageTokens, projectedMessageRemoval, observationTokens: bufferedObservationTokens, status: obsBufferStatus }, reflection: { inputObservationTokens: record.bufferedReflectionInputTokens ?? 0, observationTokens: record.bufferedReflectionTokens ?? 0, status: refBufferStatus } } }, recordId: record.id, threadId, stepNumber, generationCount: record.generationCount } }; omDebug( `[OM:status] step=${stepNumber} msgs=${pendingTokens}/${threshold} obs=${currentObservationTokens}/${effectiveObservationTokensThreshold} gen=${record.generationCount}` ); await writer.custom(statusPart).catch(() => { }); } } /** * Get the current observation status for a thread/resource. * * Loads unobserved messages from storage, counts tokens, and checks against * configured thresholds. Returns a comprehensive status object that tells the * caller what actions are needed. * * This is a pure read operation with no side effects. * * @example * ```ts * const status = await om.getStatus({ threadId }); * if (status.shouldObserve) { * await om.observe({ threadId }); * } else if (status.shouldBuffer) { * await om.buffer({ threadId }); * } * if (status.shouldReflect) { * await om.reflect(threadId); * } * ``` */ async getStatus(opts) { const { threadId, resourceId } = opts; const record = await this.getOrCreateRecord(threadId, resourceId); const currentObservationTokens = record.observationTokenCount ?? 0; let unobservedMessages; if (opts.messages) { unobservedMessages = this.getUnobservedMessages(opts.messages, record); } else { const rawMessages = await this.loadMessagesFromStorage( threadId, resourceId, record.lastObservedAt ? new Date(record.lastObservedAt) : void 0 ); unobservedMessages = this.getUnobservedMessages(rawMessages, record); } const contextWindowTokens = await this.tokenCounter.countMessagesAsync(unobservedMessages); let otherThreadTokens = 0; if (this.scope === "resource" && resourceId) { const otherContext = await this.getOtherThreadsContext(resourceId, threadId); otherThreadTokens = otherContext ? this.tokenCounter.countString(otherContext) : 0; } const pendingTokens = Math.max(0, contextWindowTokens + otherThreadTokens); const threshold = calculateDynamicThreshold(this.getEffectiveMessageTokens(record), currentObservationTokens); const bufferedChunks = getBufferedChunks(record); const bufferedChunkCount = bufferedChunks.length; const bufferedChunkTokens = bufferedChunks.reduce((sum, chunk) => sum + (chunk.messageTokens ?? 0), 0); const asyncObservationEnabled = this.buffering.isAsyncObservationEnabled(); let shouldBuffer = false; if (asyncObservationEnabled && pendingTokens < threshold) { const lockKey = this.buffering.getLockKey(threadId, resourceId); shouldBuffer = this.buffering.shouldTriggerAsyncObservation( pendingTokens, lockKey, record, this.storage, threshold ); } const shouldObserve = pendingTokens >= threshold; const reflectThreshold = getMaxThreshold(this.getEffectiveReflectionTokens(record)); const shouldReflect = currentObservationTokens >= reflectThreshold; const canActivate = bufferedChunkCount > 0; const effectiveMessageTokens = this.getEffectiveMessageTokens(record); const isSharedBudget = typeof effectiveMessageTokens !== "number"; const totalBudget = isSharedBudget ? effectiveMessageTokens.max : 0; const effectiveObservationTokensThreshold = isSharedBudget ? Math.max(totalBudget - threshold, 1e3) : reflectThreshold; const unbufferedPendingTokens = Math.max(0, pendingTokens - bufferedChunkTokens); return { record, pendingTokens, threshold, effectiveObservationTokensThreshold, unbufferedPendingTokens, shouldObserve, shouldBuffer, shouldReflect, bufferedChunkCount, bufferedChunkTokens, canActivate, asyncObservationEnabled, asyncReflectionEnabled: this.buffering.isAsyncReflectionEnabled(), scope: this.scope }; } /** * Finalize the observation lifecycle: activate any remaining buffered chunks, * then observe if the threshold is crossed. * * Call this at the end of a conversation, session, or turn sequence to ensure * no buffered observations are left orphaned and the observation cursor is * advanced. Produces a clean terminal state (no pending chunks, cursor up to date). * * @example * ```ts * // After all turns are complete * const result = await om.finalize({ threadId }); * // result.activated: true if buffered chunks were promoted * // result.observed: true if a full observation pass ran * ``` */ async finalize(opts) { const { threadId, resourceId, messages } = opts; let activated = false; let observed = false; let reflected = false; await BufferingCoordinator.awaitBuffering(threadId, resourceId ?? null, this.scope); const preStatus = await this.getStatus({ threadId, resourceId, messages }); if (preStatus.canActivate) { const actResult = await this.activate({ threadId, resourceId, messages }); activated = actResult.activated; } const postStatus = await this.getStatus({ threadId, resourceId, messages }); if (postStatus.shouldObserve) { const obsResult = await this.observe({ threadId, resourceId, messages }); observed = obsResult.observed; } const reflectStatus = await this.getStatus({ threadId, resourceId }); if (reflectStatus.shouldReflect) { const refResult = await this.reflect(threadId, resourceId); reflected = refResult.reflected; } const record = await this.getOrCreateRecord(threadId, resourceId); return { activated, observed, reflected, record }; } /** * Return only the messages that haven't been fully observed yet. * * Use this to prune observed messages from an in-memory message array, * preventing unbounded context growth across steps in a multi-step loop. * This is the array-based equivalent of the processor's `cleanupObservedContext()`. * * @example * ```ts * // In a prepareStep hook, prune before sending to the model * messages = await om.pruneObserved({ threadId, messages }); * ``` */ async pruneObserved(opts) { const { threadId, resourceId, messages } = opts; const record = await this.getOrCreateRecord(threadId, resourceId); return this.getUnobservedMessages(messages, record); } /** * Load unobserved messages from storage for a thread/resource. * * Fetches the OM record, queries storage for messages after the * lastObservedAt cursor, then applies part-level filtering so * partially-observed messages only include their unobserved parts. * * Use this when you need to load stored conversation history that * hasn't been observed yet (e.g. in a stateless gateway proxy that * only receives the latest message from the HTTP request). */ async loadUnobservedMessages(opts) { const { threadId, resourceId } = opts; const record = await this.getOrCreateRecord(threadId, resourceId); const rawMessages = await this.loadMessagesFromStorage( threadId, resourceId, record.lastObservedAt ? new Date(record.lastObservedAt) : void 0 ); return this.getUnobservedMessages(rawMessages, record); } /** * Create a buffered observation chunk without merging into active observations. * * Loads unobserved messages from storage (filtered by the buffer cursor to avoid * re-buffering), calls the observer LLM, and stores the result as a pending * buffered chunk in the DB. The chunk can later be merged into active observations * via `activate()`. * * This is a synchronous (awaited) operation — the caller decides whether to * `await` it or fire-and-forget. All state lives in storage; no in-process * coordination is needed. * * @example * ```ts * const status = await om.getStatus({ threadId }); * if (status.shouldBuffer) { * await om.buffer({ threadId }); * } * ``` */ /** @internal Used by ObservationStep. */ async buffer(opts) { const { threadId, resourceId, requestContext, observabilityContext } = opts; let record = opts.record ?? await this.getOrCreateRecord(threadId, resourceId); if (!this.buffering.isAsyncObservationEnabled()) { return { buffered: false, record }; } if (record.isBufferingObservation && isOpActiveInProcess(record.id, "bufferingObservation")) { return { buffered: false, record }; } const currentTokens = opts.pendingTokens ?? record.pendingMessageTokens ?? 0; const lockKey = this.buffering.getLockKey(threadId, resourceId); const bufferKey = this.buffering.getObservationBufferKey(lockKey); BufferingCoordinator.lastBufferedBoundary.set(bufferKey, currentTokens); if (record.isBufferingObservation) { await this.storage.setBufferingObservationFlag(record.id, false).catch(() => { }); } const existingOp = BufferingCoordinator.asyncBufferingOps.get(bufferKey); if (existingOp) { try { await existingOp; } catch { } } registerOp(record.id, "bufferingObservation"); this.storage.setBufferingObservationFlag(record.id, true, currentTokens).catch((err) => { omError("[OM] Failed to set buffering observation flag", err); }); let resolveOp; const opPromise = new Promise((resolve) => { resolveOp = resolve; }); BufferingCoordinator.asyncBufferingOps.set(bufferKey, opPromise); record = await this.storage.getObservationalMemory(record.threadId, record.resourceId) ?? record; let flagCleared = false; try { let candidateMessages; if (opts.messages) { candidateMessages = this.getUnobservedMessages(opts.messages, record, { excludeBuffered: true }); } else { const rawMessages = await this.loadMessagesFromStorage( threadId, resourceId, record.lastObservedAt ? new Date(record.lastObservedAt) : void 0 ); candidateMessages = this.getUnobservedMessages(rawMessages, record, { excludeBuffered: true }); } if (!opts.messages) { let bufferCursor = BufferingCoordinator.lastBufferedAtTime.get(bufferKey) ?? record.lastBufferedAtTime ?? null; if (record.lastObservedAt) { const lastObserved = new Date(record.lastObservedAt); if (!bufferCursor || lastObserved > bufferCursor) { bufferCursor = lastObserved; } } if (bufferCursor) { candidateMessages = candidateMessages.filter((msg) => { if (!msg.createdAt) return true; return new Date(msg.createdAt) > bufferCursor; }); } } const bufferTokens = this.observationConfig.bufferTokens ?? 5e3; const minNewTokens = bufferTokens / 2; const newTokens = await this.tokenCounter.countMessagesAsync(candidateMessages); if (candidateMessages.length === 0 || !opts.skipMinimumTokenCheck && newTokens < minNewTokens) { return { buffered: false, record }; } if (opts.beforeBuffer) { await opts.beforeBuffer(candidateMessages); } else if (opts.messages) { this.sealMessagesForBuffering(candidateMessages); } const cycleId = `buffer-obs-${Date.now()}-${Math.random().toString(36).slice(2, 11)}`; const startedAt = (/* @__PURE__ */ new Date()).toISOString(); const startMarker = createBufferingStartMarker({ cycleId, operationType: "observation", tokensToBuffer: newTokens, recordId: record.id, threadId, threadIds: [threadId], config: this.getObservationMarkerConfig() }); await this.persistMarkerToStorage(startMarker, threadId, record.resourceId ?? void 0); const writer = opts.writer; if (writer) { void writer.custom({ ...startMarker, transient: true }).catch(() => { }); } await ObservationStrategy.create(this, { record, threadId, resourceId: record.resourceId ?? void 0, messages: candidateMessages, cycleId, startedAt, writer, sendSignal: opts.sendSignal, requestContext, currentModel: opts.currentModel, observabilityContext }).run(); if (isOmReproCaptureEnabled()) { writeObserverExchangeReproCapture({ threadId, resourceId: record.resourceId ?? void 0, label: `buffer-${cycleId}`, observerExchange: this.observer.lastExchange, details: { cycleId, startedAt, buffered: true, candidateMessageIds: candidateMessages.map((message) => message.id), candidateMessageCount: candidateMessages.length, pendingTokens: currentTokens, newTokens } }); } await this.storage.setBufferingObservationFlag(record.id, false, newTokens).catch(() => { }); flagCleared = true; BufferingCoordinator.lastBufferedBoundary.set(bufferKey, newTokens); const maxTimestamp = this.getMaxMessageTimestamp(candidateMessages); const cursor = new Date(maxTimestamp.getTime() + 1); BufferingCoordinator.lastBufferedAtTime.set(bufferKey, cursor); const updatedRecord = await this.getOrCreateRecord(threadId, resourceId); return { buffered: true, record: updatedRecord }; } catch (error) { omError("[OM] buffer() failed", error); return { buffered: false, record }; } finally { unregisterOp(record.id, "bufferingObservation"); BufferingCoordinator.asyncBufferingOps.delete(bufferKey); resolveOp(); if (!flagCleared) { await this.storage.setBufferingObservationFlag(record.id, false).catch(() => { }); } } } /** * Activate buffered observation chunks by merging them into active observations. * * This is a pure storage operation — no LLM call. It reads buffered chunks from * the DB and swaps them into active observations via `storage.swapBufferedToActive()`. * * Call this after `buffer()` has created chunks, typically at the start of a new * turn or when `getStatus().canActivate` is true. * * @example * ```ts * const status = await om.getStatus({ threadId }); * if (status.canActivate) { * const result = await om.activate({ threadId }); * if (result.activated) { * console.log('Activated', result.activatedMessageIds?.length, 'message observations'); * } * } * ``` */ /** @internal Used by ObservationStep. */ async activate(opts) { const { threadId, resourceId } = opts; const record = await this.getOrCreateRecord(threadId, resourceId); if (this.buffering.isAsyncObservationEnabled()) { const lockKey = this.buffering.getLockKey(threadId, resourceId); const bufKey = this.buffering.getObservationBufferKey(lockKey); const dbBoundary = record.lastBufferedAtTokens ?? 0; if (dbBoundary > 0 && opts.messages) { const unobserved = this.getUnobservedMessages(opts.messages, record); const currentContextTokens = this.tokenCounter.countMessages(unobserved); if (currentContextTokens < dbBoundary * 0.5) { omDebug( `[OM:activate] resetting stale lastBufferedBoundary: dbBoundary=${dbBoundary}, currentContextTokens=${currentContextTokens}` ); BufferingCoordinator.lastBufferedBoundary.set(bufKey, 0); await this.storage.setBufferingObservationFlag(record.id, false, 0).catch(() => { }); } } } const chunks = getBufferedChunks(record); if (!chunks.length) { return { activated: false, record }; } let activationTriggeredBy = "threshold"; let activationLastActivityAt; let activationActivateAfterIdle; let activateAfterIdleExpiredMs; let previousModel; let currentModel; if (opts.checkThreshold) { const thresholdMessages = opts.messages ?? await this.loadMessagesFromStorage( threadId, resourceId, record.lastObservedAt ? new Date(record.lastObservedAt) : void 0 ); const activateAfterIdle = resolveActivationTTL(this.observationConfig.activateAfterIdle, opts.currentModel); const lastActivityAt = getLastActivityFromMessages(thresholdMessages); const ttlExpiredMs = activateAfterIdle !== void 0 && lastActivityAt !== void 0 ? Date.now() - lastActivityAt : void 0; const ttlExpired = ttlExpiredMs !== void 0 && activateAfterIdle !== void 0 && ttlExpiredMs >= activateAfterIdle; const actorModel = getCurrentModel2(opts.currentModel); const lastModel = getLastModelFromMessages(thresholdMessages); const providerChanged = this.observationConfig.activateOnProviderChange === true && didProviderChange(actorModel, lastModel); if (providerChanged) { activationTriggeredBy = "provider_change"; previousModel = lastModel; currentModel = actorModel; } else if (ttlExpired) { activationTriggeredBy = "ttl"; activationLastActivityAt = lastActivityAt; activationActivateAfterIdle = activateAfterIdle; activateAfterIdleExpiredMs = ttlExpiredMs; } else { const status = await this.getStatus({ threadId, resourceId, messages: thresholdMessages }); if (status.pendingTokens < status.threshold) { return { activated: false, record }; } } } if (record.isBufferingObservation) { const lockKey = this.buffering.getLockKey(threadId, resourceId); const bufferKey = this.buffering.getObservationBufferKey(lockKey); const asyncOp = BufferingCoordinator.asyncBufferingOps.get(bufferKey); if (asyncOp) { try { await Promise.race([ asyncOp, new Promise((_, reject) => setTimeout(() => reject(new Error("Timeout")), 6e4)) ]); } catch { } } } const freshRecord = await this.storage.getObservationalMemory(record.threadId, record.resourceId); if (!freshRecord) { return { activated: false, record }; } const freshChunks = getBufferedChunks(freshRecord); if (!freshChunks.length) { return { activated: false, record }; } const messageTokensThreshold = getMaxThreshold(this.getEffectiveMessageTokens(freshRecord)); const bufferActivation = this.observationConfig.bufferActivation ?? 0.7; const activationRatio = resolveActivationRatio(bufferActivation, messageTokensThreshold); const totalChunkMessageTokens = freshChunks.reduce((sum, c) => sum + (c.messageTokens ?? 0), 0); const currentPendingTokens = freshRecord.pendingMessageTokens || totalChunkMessageTokens; const forceMaxActivation = !!(this.observationConfig.blockAfter && currentPendingTokens >= this.observationConfig.blockAfter); const activationResult = await this.storage.swapBufferedToActive({ id: freshRecord.id, activationRatio, messageTokensThreshold, currentPendingTokens, forceMaxActivation, bufferedChunks: freshChunks }); await this.storage.setBufferingObservationFlag(freshRecord.id, false).catch(() => { }); unregisterOp(freshRecord.id, "bufferingObservation"); const postSwapRecord = await this.storage.getObservationalMemory(record.threadId, record.resourceId); if (opts.writer && postSwapRecord && activationResult.activatedCycleIds.length > 0) { const perChunkMap = new Map(activationResult.perChunk?.map((c) => [c.cycleId, c])); for (const cycleId of activationResult.activatedCycleIds) { const chunkData = perChunkMap.get(cycleId); const activationMarker = createActivationMarker({ cycleId, operationType: "observation", chunksActivated: 1, tokensActivated: chunkData?.messageTokens ?? activationResult.messageTokensActivated, observationTokens: chunkData?.observationTokens ?? activationResult.observationTokensActivated, messagesActivated: chunkData?.messageCount ?? activationResult.messagesActivated, recordId: postSwapRecord.id, threadId: postSwapRecord.threadId ?? record.threadId ?? "", generationCount: postSwapRecord.generationCount ?? 0, observations: chunkData?.observations ?? activationResult.observations, triggeredBy: activationTriggeredBy, lastActivityAt: activationLastActivityAt, ttlExpiredMs: activateAfterIdleExpiredMs, previousModel, currentModel, config: { ...this.getObservationMarkerConfig(), activateAfterIdle: activationActivateAfterIdle ?? this.observationConfig.activateAfterIdle } }); void opts.writer.custom({ ...activationMarker, transient: true }).catch(() => { }); await this.persistMarkerToMessage( activationMarker, opts.messageList, record.threadId ?? "", record.resourceId ?? void 0 ); } } const thread = await this.storage.getThreadById({ threadId }); if (thread) { const activatedChunks = freshChunks.filter((c) => activationResult.activatedCycleIds.includes(c.cycleId)); const lastActivated = activatedChunks[activatedChunks.length - 1]; if (lastActivated) { const chunkThreadTitle = lastActivated.threadTitle; const newMetadata = memory.setThreadOMMetadata(thread.metadata, { suggestedResponse: lastActivated.suggestedContinuation, currentTask: lastActivated.currentTask, threadTitle: chunkThreadTitle }); const oldTitle = thread.title?.trim(); const newTitle = chunkThreadTitle?.trim(); const shouldUpdateThreadTitle = !!newTitle && newTitle.length >= 3 && newTitle !== oldTitle; await this.storage.updateThread({ id: threadId, title: shouldUpdateThreadTitle ? newTitle : thread.title ?? "", metadata: newMetadata }); } } const updatedRecord = await this.getOrCreateRecord(threadId, resourceId); return { activated: true, record: updatedRecord, activatedMessageIds: activationResult.activatedMessageIds }; } /** * Manually trigger observation. * * When `messages` is provided, those are used directly (filtered for unobserved) * instead of reading from storage. This allows external systems (e.g., opencode) * to pass conversation messages without duplicating them into Mastra's DB. * * Returns a result indicating whether observation and/or reflection occurred, * along with the updated record. */ async observe(opts) { const { threadId, resourceId, messages, hooks, requestContext } = opts; const lockKey = this.buffering.getLockKey(threadId, resourceId); const reflectionHooks = hooks ? { onReflectionStart: hooks.onReflectionStart, onReflectionEnd: hooks.onReflectionEnd } : void 0; let observed = false; let observationUsage; let generationBefore = -1; await this.withLock(lockKey, async () => { const freshRecord = await this.getOrCreateRecord(threadId, resourceId); generationBefore = freshRecord.generationCount; const unobservedMessages = messages ? this.getUnobservedMessages(messages, freshRecord) : await this.loadMessagesFromStorage( threadId, resourceId, freshRecord.lastObservedAt ? new Date(freshRecord.lastObservedAt) : void 0 ); if (!this.meetsObservationThreshold({ record: freshRecord, unobservedTokens: await this.tokenCounter.countMessagesAsync(unobservedMessages) })) { return; } hooks?.onObservationStart?.(); let observationError; try { const result = await ObservationStrategy.create(this, { record: freshRecord, threadId, resourceId, messages: unobservedMessages, reflectionHooks, requestContext, writer: opts.writer, observabilityContext: opts.observabilityContext }).run(); observed = result.observed; observationUsage = result.usage; } catch (error) { observationError = error instanceof Error ? error : new Error(String(error)); throw error; } finally { hooks?.onObservationEnd?.({ usage: observationUsage, error: observationError }); } }); const record = await this.getOrCreateRecord(threadId, resourceId); const reflected = record.generationCount > generationBefore && generationBefore >= 0; return { observed, reflected, record }; } /** * Manually trigger reflection with optional guidance prompt. * * @example * ```ts * // Trigger reflection with specific focus * await om.reflect(threadId, resourceId, * "focus on the authentication implementation, only keep minimal details about UI styling" * ); * ``` */ async reflect(threadId, resourceId, prompt, requestContext, observabilityContext) { const record = await this.getOrCreateRecord(threadId, resourceId); if (!record.activeObservations) { return { reflected: false, record, usage: void 0 }; } await this.storage.setReflectingFlag(record.id, true); registerOp(record.id, "reflecting"); try { const reflectThreshold = getMaxThreshold(this.getEffectiveReflectionTokens(record)); const reflectResult = await this.reflector.call( record.activeObservations, prompt, void 0, reflectThreshold, void 0, void 0, void 0, requestContext, observabilityContext, void 0 ); const reflectionTokenCount = this.tokenCounter.countObservations(reflectResult.observations); await this.storage.createReflectionGeneration({ currentRecord: record, reflection: reflectResult.observations, tokenCount: reflectionTokenCount }); const updatedRecord = await this.getOrCreateRecord(threadId, resourceId); return { reflected: true, record: updatedRecord, usage: reflectResult.usage }; } catch (error) { omError("[OM] reflect() failed", error); const latestRecord = await this.getOrCreateRecord(threadId, resourceId); return { reflected: false, record: latestRecord, usage: void 0 }; } finally { await this.storage.setReflectingFlag(record.id, false); unregisterOp(record.id, "reflecting"); } } /** * Get current observations for a thread/resource */ async getObservations(threadId, resourceId) { const ids = this.getStorageIds(threadId, resourceId); const record = await this.storage.getObservationalMemory(ids.threadId, ids.resourceId); return record?.activeObservations; } /** * Get current record for a thread/resource */ async getRecord(threadId, resourceId) { const ids = this.getStorageIds(threadId, resourceId); return this.storage.getObservationalMemory(ids.threadId, ids.resourceId); } /** * Update per-record config overrides for observation and/or reflection thresholds. * The provided config is deep-merged into the record's `_overrides` key, * so you only need to specify the fields you want to change. * * Overrides that violate buffering invariants (e.g. messageTokens below * bufferTokens) are silently ignored at read time — the helpers fall back * to the instance-level config. * * @example * ```ts * await om.updateRecordConfig('thread-1', undefined, { * observation: { messageTokens: 2000 }, * reflection: { observationTokens: 8000 }, * }); * ``` */ async updateRecordConfig(threadId, resourceId, config) { const ids = this.getStorageIds(threadId, resourceId); const record = await this.storage.getObservationalMemory(ids.threadId, ids.resourceId); if (!record) { throw new Error(`No observational memory record found for thread ${ids.threadId}`); } await this.storage.updateObservationalMemoryConfig({ id: record.id, config: { _overrides: config } }); } /** * Get observation history (previous generations) */ async getHistory(threadId, resourceId, limit, options) { const ids = this.getStorageIds(threadId, resourceId); return this.storage.getObservationalMemoryHistory(ids.threadId, ids.resourceId, limit, options); } /** * Clear all memory for a specific thread/resource */ async clear(threadId, resourceId) { const ids = this.getStorageIds(threadId, resourceId); await this.storage.clearObservationalMemory(ids.threadId, ids.resourceId); this.buffering.cleanupStaticMaps(ids.threadId ?? ids.resourceId, ids.resourceId); } /** * Get the underlying storage adapter */ getStorage() { return this.storage; } /** * Get the token counter */ getTokenCounter() { return this.tokenCounter; } /** * Get current observation configuration */ getObservationConfig() { return this.observationConfig; } /** * Get current reflection configuration */ getReflectionConfig() { return this.reflectionConfig; } /** * Get the message history instance for marker persistence. */ getMessageHistory() { return this.messageHistory; } /** * Get whether thread IDs should be obscured in observations. */ getObscureThreadIds() { return this.shouldObscureThreadIds; } /** * Begin a new observation turn — the high-level API for managing the * observe/buffer/activate/reflect lifecycle across agentic loop steps. * * @example * ```ts * const turn = om.beginTurn({ threadId, resourceId, messageList }); * await turn.start(memory); * * const step0 = turn.step(0); * const ctx = await step0.prepare(); * // ... agent generates ... * * await turn.end(); * ``` */ beginTurn(opts) { return new ObservationTurn({ om: this, threadId: opts.threadId, resourceId: opts.resourceId, messageList: opts.messageList, observabilityContext: opts.observabilityContext, hooks: opts.hooks }); } }; // src/processors/observational-memory/temporal-markers.ts var TEMPORAL_GAP_REMINDER_TYPE = "temporal-gap"; function getTemporalGapReminderText(gapText, timestamp) { return `${gapText} \u2014 ${formatTemporalTimestamp(new Date(timestamp))}`; } function getTemporalGapReminderMetadata(message, gapText, gapMs, timestamp) { const formattedTimestamp = formatTemporalTimestamp(new Date(timestamp)); return { reminderType: TEMPORAL_GAP_REMINDER_TYPE, gapText, gapMs, timestamp: formattedTimestamp, timestampMs: timestamp, precedesMessageId: message.id, systemReminder: { type: TEMPORAL_GAP_REMINDER_TYPE, message: getTemporalGapReminderText(gapText, timestamp), gapText, gapMs, timestamp: formattedTimestamp, timestampMs: timestamp, precedesMessageId: message.id } }; } function getTemporalGapReminderAttributes(message, gapText, gapMs, timestamp) { return { type: TEMPORAL_GAP_REMINDER_TYPE, gapText, gapMs, timestamp: formatTemporalTimestamp(new Date(timestamp)), timestampMs: timestamp, precedesMessageId: message.id }; } function isTemporalGapMarkerForMessage(message, targetMessageId) { if (!isTemporalGapMarker(message)) { return false; } const metadata = message.content.metadata; if (metadata?.precedesMessageId === targetMessageId) { return true; } return metadata?.systemReminder?.type === TEMPORAL_GAP_REMINDER_TYPE && metadata.systemReminder.precedesMessageId === targetMessageId; } async function insertTemporalGapMarkers({ messageList, sendSignal }) { const inputMessages = messageList.get.input.db().filter((message) => Boolean(message)); const latestInputMessage = inputMessages.at(-1); if (!latestInputMessage || isTemporalGapMarker(latestInputMessage)) { return; } const allMessages = messageList.get.all.db().filter((message) => Boolean(message)); const latestInputIndex = allMessages.findIndex((message) => message.id === latestInputMessage.id); if (latestInputIndex <= 0) { return; } if (allMessages.some((message) => isTemporalGapMarkerForMessage(message, latestInputMessage.id))) { return; } let previousNonMarker; for (let index = latestInputIndex - 1; index >= 0; index--) { const candidate = allMessages[index]; if (candidate && !isTemporalGapMarker(candidate)) { previousNonMarker = candidate; break; } } if (!previousNonMarker) { return; } const timestamp = getMessagePartTimestamp(latestInputMessage, "first"); const gapMs = timestamp - getMessagePartTimestamp(previousNonMarker, "last"); const gapText = formatTemporalGap(gapMs); if (!gapText) { return; } await sendSignal?.({ id: `__temporal_gap_${crypto.randomUUID()}`, type: "reactive", tagName: "system-reminder", contents: getTemporalGapReminderText(gapText, timestamp), createdAt: new Date(timestamp - 1), acceptedAt: new Date(timestamp), attributes: getTemporalGapReminderAttributes(latestInputMessage, gapText, gapMs, timestamp), metadata: getTemporalGapReminderMetadata(latestInputMessage, gapText, gapMs, timestamp) }); } // src/processors/observational-memory/processor.ts function getOmObservabilityContext(args) { if (!args.tracing || !args.tracingContext || !args.loggerVNext || !args.metrics) { return void 0; } return { tracing: args.tracing, tracingContext: args.tracingContext, loggerVNext: args.loggerVNext, metrics: args.metrics }; } var GATEWAY_STATE_KEY = "__isGatewayModel"; function isMastraGatewayModel(model) { return typeof model === "object" && model !== null && "gatewayId" in model && model.gatewayId === "mastra"; } function injectObservationContextMessages({ messageList, systemMessages, continuationMessage, threadId, resourceId }) { if (!systemMessages?.length) { return; } messageList.clearSystemMessages("observational-memory"); for (const msg of systemMessages) { messageList.addSystem(msg, "observational-memory"); } const contMsg = continuationMessage ?? { id: "om-continuation", role: "user", createdAt: /* @__PURE__ */ new Date(0), content: { format: 2, parts: [{ type: "text", text: `${chunkD4J4XPGM_cjs.OBSERVATION_CONTINUATION_HINT}` }] }, threadId, resourceId }; messageList.add(contMsg, "memory"); } var ObservationalMemoryProcessor = class { id = "observational-memory"; name = "Observational Memory"; /** The underlying ObservationalMemory engine. */ engine; /** Memory instance for loading context. */ memory; /** Whether temporal-gap reminder markers should be inserted. */ temporalMarkers; /** Active turn — created on first processInputStep, ended on processOutputResult. */ turn; constructor(engine, memory, options) { this.engine = engine; this.memory = memory; this.temporalMarkers = options?.temporalMarkers ?? false; } // ─── Processor lifecycle hooks ────────────────────────────────────────── async processInputStep(args) { const { messageList, requestContext, stepNumber, state: _state, writer, model, abortSignal, abort, rotateResponseMessageId } = args; const state = _state ?? {}; omDebug( `[OM:processInputStep:ENTER] step=${stepNumber}, hasMastraMemory=${!!requestContext?.get("MastraMemory")}, hasMemoryInfo=${!!messageList?.serialize()?.memoryInfo?.threadId}` ); const context = this.engine.getThreadContext(requestContext, messageList); if (!context) { omDebug(`[OM:processInputStep:NO-CONTEXT] getThreadContext returned null \u2014 returning early`); return messageList; } if (isMastraGatewayModel(model)) { state[GATEWAY_STATE_KEY] = true; omDebug(`[OM:processInputStep:GATEWAY] gateway handles OM \u2014 skipping local processing`); return messageList; } const { threadId, resourceId } = context; const memoryContext = memory.parseMemoryRequestContext(requestContext); const readOnly = memoryContext?.memoryConfig?.readOnly; const actorModelContext = model?.modelId ? { provider: model.provider, modelId: model.modelId, providerOptions: args.providerOptions } : void 0; state.__omActorModelContext = actorModelContext; return this.engine.getTokenCounter().runWithModelContext(actorModelContext, async () => { const reproCaptureEnabled = isOmReproCaptureEnabled(); const preRecordSnapshot = reproCaptureEnabled ? safeCaptureJson(await this.engine.getOrCreateRecord(threadId, resourceId)) : null; const preMessagesSnapshot = reproCaptureEnabled ? safeCaptureJson(messageList.get.all.db()) : null; const preSerializedMessageList = reproCaptureEnabled ? safeCaptureJson(messageList.serialize()) : null; if (readOnly) { const ctx = await loadMemoryContextMessages({ memory: this.memory, messageList, threadId, resourceId }); const systemMessages = ctx.hasObservations && ctx.omRecord ? await this.engine.buildContextSystemMessages({ threadId, resourceId, record: ctx.omRecord, unobservedContextBlocks: ctx.otherThreadsContext }) : void 0; injectObservationContextMessages({ messageList, systemMessages, continuationMessage: ctx.continuationMessage, threadId, resourceId }); return messageList; } const activeTurn = state.__omTurn ?? this.turn; if (activeTurn && activeTurn.messageList !== messageList) { await activeTurn.end().catch(() => { }); if (this.turn === activeTurn) { this.turn = void 0; } state.__omTurn = void 0; } if (!this.turn || !state.__omTurn) { if (this.turn && !state.__omTurn) { await this.turn.end().catch(() => { }); } this.turn = this.engine.beginTurn({ threadId, resourceId, messageList, observabilityContext: getOmObservabilityContext(args), hooks: { onBufferChunkSealed: rotateResponseMessageId, onSyncObservationComplete: rotateResponseMessageId } }); this.turn.writer = writer; this.turn.sendSignal = args.sendSignal; this.turn.requestContext = requestContext; await this.turn.start(this.memory); if (stepNumber === 0 && this.temporalMarkers) { await insertTemporalGapMarkers({ messageList, sendSignal: args.sendSignal }); } state.__omTurn = this.turn; } this.turn.addHooks({ onBufferChunkSealed: rotateResponseMessageId, onSyncObservationComplete: rotateResponseMessageId }); const observabilityContext = getOmObservabilityContext(args); state.__omObservabilityContext = observabilityContext; this.turn.observabilityContext = observabilityContext; this.turn.actorModelContext = actorModelContext; { const step = this.turn.step(stepNumber); let ctx; try { ctx = await step.prepare(); } catch (error) { const err = error instanceof Error ? error : new Error(String(error)); const abortMessage = abortSignal?.aborted ? "Agent execution was aborted" : `Encountered error during memory observation: ${err.message}`; if (typeof abort === "function") { abort(abortMessage); } throw err; } injectObservationContextMessages({ messageList, systemMessages: ctx.systemMessage, continuationMessage: this.turn.context.continuation, threadId, resourceId }); const freshRecord = await this.engine.getOrCreateRecord(threadId, resourceId); await this.engine.emitProgress({ record: freshRecord, stepNumber, pendingTokens: ctx.status.pendingTokens, threshold: ctx.status.threshold, effectiveObservationTokensThreshold: ctx.status.effectiveObservationTokensThreshold, currentObservationTokens: freshRecord.observationTokenCount ?? 0, writer, threadId, resourceId }); const allDbMsgs = messageList.get.all.db(); const tokenCounter = this.engine.getTokenCounter(); const contextTokens = await tokenCounter.countMessagesAsync(allDbMsgs); const otherThreadsContext = this.turn.context.otherThreadsContext; const otherThreadTokens = otherThreadsContext ? tokenCounter.countString(otherThreadsContext) : 0; const finalTotalPending = contextTokens + otherThreadTokens; await this.engine.getStorage().setPendingMessageTokens(freshRecord.id, finalTotalPending).catch(() => { }); if (reproCaptureEnabled) { writeProcessInputStepReproCapture({ threadId, resourceId, stepNumber, args, preRecord: preRecordSnapshot, postRecord: safeCaptureJson(freshRecord), preMessages: preMessagesSnapshot, preBufferedChunks: [], preContextTokenCount: 0, preSerializedMessageList, postBufferedChunks: [], postContextTokenCount: finalTotalPending, messageList, details: {}, observerExchange: ctx.observerExchange }); } } return messageList; }); } async processOutputResult(args) { const { messageList, requestContext, state: _state } = args; const state = _state ?? {}; const context = this.engine.getThreadContext(requestContext, messageList); if (!context) return messageList; if (state[GATEWAY_STATE_KEY]) return messageList; const observabilityContext = getOmObservabilityContext(args); state.__omObservabilityContext = observabilityContext; return this.engine.getTokenCounter().runWithModelContext(state.__omActorModelContext, async () => { const memoryContext = memory.parseMemoryRequestContext(requestContext); if (memoryContext?.memoryConfig?.readOnly) return messageList; const turn = state.__omTurn ?? this.turn; if (turn) { await turn.end(); this.turn = void 0; state.__omTurn = void 0; } else { const newOutput = messageList.get.response.db(); const newInput = messageList.get.input.db(); const messagesToSave = [...newInput, ...newOutput]; if (messagesToSave.length > 0 && context.threadId) { await this.engine.persistMessages(messagesToSave, context.threadId, context.resourceId); } } return messageList; }); } // ─── Passthrough API ──────────────────────────────────────────────────── get config() { return this.engine.config; } async waitForBuffering(threadId, resourceId, timeoutMs) { return this.engine.waitForBuffering(threadId, resourceId, timeoutMs); } async getResolvedConfig(requestContext) { return this.engine.getResolvedConfig(requestContext); } }; // src/processors/observational-memory/observation-utils.ts var BOUNDARY_WITH_DATE_RE = /\n{2,}--- message boundary \((\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z)\) ---\n{2,}/; function getObservationsAsOf(activeObservations, asOf) { const trimmed = activeObservations.trim(); if (!trimmed) return ""; const parts = trimmed.split(BOUNDARY_WITH_DATE_RE); const chunks = []; const firstChunk = parts[0]?.trim(); if (firstChunk) { chunks.push(firstChunk); } for (let i = 1; i < parts.length; i += 2) { const dateStr = parts[i]; const chunk = parts[i + 1]?.trim(); if (!chunk) continue; const boundaryDate = new Date(dateStr); if (isNaN(boundaryDate.getTime())) continue; if (boundaryDate <= asOf) { chunks.push(chunk); } } return chunks.join("\n\n"); } exports.ModelByInputTokens = ModelByInputTokens; exports.OBSERVER_SYSTEM_PROMPT = OBSERVER_SYSTEM_PROMPT; exports.ObservationalMemory = ObservationalMemory; exports.ObservationalMemoryProcessor = ObservationalMemoryProcessor; exports.TokenCounter = TokenCounter; exports.buildObserverPrompt = buildObserverPrompt; exports.buildObserverSystemPrompt = buildObserverSystemPrompt; exports.combineObservationGroupRanges = combineObservationGroupRanges; exports.deriveObservationGroupProvenance = deriveObservationGroupProvenance; exports.extractCurrentTask = extractCurrentTask; exports.formatMessagesForObserver = formatMessagesForObserver; exports.formatToolResultForObserver = formatToolResultForObserver; exports.getObservationsAsOf = getObservationsAsOf; exports.hasCurrentTaskSection = hasCurrentTaskSection; exports.injectAnchorIds = injectAnchorIds; exports.optimizeObservationsForContext = optimizeObservationsForContext; exports.parseAnchorId = parseAnchorId; exports.parseObservationGroups = parseObservationGroups; exports.parseObserverOutput = parseObserverOutput; exports.reconcileObservationGroupsFromReflection = reconcileObservationGroupsFromReflection; exports.renderObservationGroupsForReflection = renderObservationGroupsForReflection; exports.resolveToolResultValue = resolveToolResultValue; exports.stripEphemeralAnchorIds = stripEphemeralAnchorIds; exports.stripObservationGroups = stripObservationGroups; exports.truncateStringByTokens = truncateStringByTokens; exports.wrapInObservationGroup = wrapInObservationGroup; //# sourceMappingURL=chunk-2ZNDDQ6X.cjs.map //# sourceMappingURL=chunk-2ZNDDQ6X.cjs.map