{"version":3,"file":"memoryFlushPhase.mjs","sources":["../../../../src/graphs/phases/memoryFlushPhase.ts"],"sourcesContent":["/**\n * Memory flush phase — trigger logic + reflection invocation.\n *\n * Ported from upstream's post-turn flush handler. The agent is re-invoked\n * with a reflection system prompt and the `memory_append` tool unlocked;\n * it writes notes to its future self, then the graph returns to normal.\n *\n * This module is INTENTIONALLY decoupled from the specific graph runtime —\n * it exposes pure functions (`shouldFlushMemory`) plus a runner that takes\n * the model as a parameter, so the same logic works from `createAgentNode`,\n * `MultiAgentGraph`, or a future graph backend that wants to override\n * flush behaviour.\n *\n * The agentic tool-execution loop (invoke → run tool_calls → feed results\n * back → repeat until stop) lives in {@link ./flushLoop}. This module wires\n * it up: build tools, resolve prompts, flip the phase, call the loop,\n * shape the rich result.\n */\nimport type { BaseChatModel } from '@langchain/core/language_models/chat_models';\nimport { HumanMessage, SystemMessage } from '@langchain/core/messages';\nimport {\n  DEFAULT_FLUSH_RESERVE_FLOOR_TOKENS,\n  DEFAULT_FLUSH_SOFT_THRESHOLD_TOKENS,\n  DEFAULT_MAX_FLUSH_ITERATIONS,\n  MEMORY_PHASE_FLUSHING,\n  MEMORY_PHASE_NORMAL,\n} from '@/memory/constants';\nimport type { MemoryConfig } from '@/memory/types';\nimport { buildMemoryTools } from '@/tools/memory';\nimport { resolveFlushPrompts } from '@/prompts/memoryFlushPrompt';\nimport {\n  bindToolsIfSupported,\n  runFlushLoop,\n  type FlushLoopResult,\n  type ToolErrorRecord,\n} from './flushLoop';\n\nexport interface ShouldFlushInput {\n  currentTokens: number;\n  windowTokens: number;\n  reserveFloorTokens?: number;\n  softThresholdTokens?: number;\n}\n\n/**\n * Pure trigger function: fires when the current context is within\n * `softThreshold + reserveFloor` tokens of the model window. Matches\n * upstream's formula.\n */\nexport function shouldFlushMemory(input: ShouldFlushInput): boolean {\n  if (\n    !Number.isFinite(input.currentTokens) ||\n    !Number.isFinite(input.windowTokens)\n  ) {\n    return false;\n  }\n  if (input.windowTokens <= 0) return false;\n  const reserve =\n    input.reserveFloorTokens ?? DEFAULT_FLUSH_RESERVE_FLOOR_TOKENS;\n  const soft = input.softThresholdTokens ?? DEFAULT_FLUSH_SOFT_THRESHOLD_TOKENS;\n  return input.currentTokens >= input.windowTokens - reserve - soft;\n}\n\nexport interface RunFlushParams {\n  model: BaseChatModel;\n  memory: MemoryConfig;\n  /** A compact summary of the conversation — last N turns, not raw history. */\n  conversationSummary: string;\n  /**\n   * Accessor the graph runtime uses to expose the current phase to the\n   * append tool. The runner sets this to `memory_flushing` for the duration\n   * of the reflection turn and restores it on exit.\n   */\n  setPhase: (\n    phase: typeof MEMORY_PHASE_NORMAL | typeof MEMORY_PHASE_FLUSHING\n  ) => void;\n  /**\n   * @deprecated No longer used — the 8-path canonical-document model\n   * does not date-stamp files. Kept in the params shape for one release\n   * so host's callers don't break on the type signature.\n   */\n  timezone?: string;\n  /**\n   * @deprecated Same as `timezone` — unused in the canonical-document model.\n   */\n  nowMs?: number;\n  /** Override for the agentic-loop iteration cap. */\n  maxIterations?: number;\n  /**\n   * Optional per-iteration callback for structured debug logging.\n   * Caller is expected to demote this to debug once the rollout is stable.\n   */\n  onIteration?: (event: {\n    i: number;\n    toolCallCount: number;\n    toolNames: string[];\n  }) => void;\n}\n\nexport interface RunFlushResult {\n  /** Did the flush actually run (false if disabled via config). */\n  ran: boolean;\n  /** Model.invoke() iterations actually performed. */\n  iterations?: number;\n  /** Total `memory_append` calls the model emitted. */\n  appendsAttempted?: number;\n  /** Subset that returned `{ ok: true }` from the backend. */\n  appendsSucceeded?: number;\n  /** Tool errors the model saw during the flush — surfaced for logging. */\n  toolErrors?: ToolErrorRecord[];\n  /** Final text reply equalled SILENT_REPLY_TOKEN (`NO_REPLY`). */\n  silentReply?: boolean;\n  /** Loop hit its iteration cap with tool_calls still pending. */\n  hitIterationCap?: boolean;\n  /** Final text reply from the last AIMessage. */\n  finalText?: string;\n  /** Error message if the flush threw. */\n  error?: string;\n}\n\n/**\n * Run the reflection turn. The model is re-invoked with just the flush\n * prompt + compact summary + the append tool unlocked. We intentionally\n * drop the full history — the prompt tells the agent to write notes based\n * on what it learned, not to continue the conversation.\n *\n * The loop keeps invoking the model until it stops emitting tool_calls,\n * hits the iteration cap, or replies with {@link SILENT_REPLY_TOKEN}. Each\n * `memory_append` tool_call is executed against the pgvector backend and\n * the result (success path or error) is fed back as a ToolMessage so the\n * model can self-correct (e.g. retry with a valid path).\n */\nexport async function runMemoryFlush(\n  params: RunFlushParams\n): Promise<RunFlushResult> {\n  const {\n    model,\n    memory,\n    conversationSummary,\n    setPhase,\n    maxIterations,\n    onIteration,\n  } = params;\n  if (memory.flush?.enabled === false) {\n    return { ran: false };\n  }\n\n  setPhase(MEMORY_PHASE_FLUSHING);\n  try {\n    const tools = buildMemoryTools({\n      ...memory,\n      readEnabled: false,\n      writeEnabled: true,\n      getPhase: () => MEMORY_PHASE_FLUSHING,\n    });\n\n    // Bind tools to the caller-provided model. If the model already came\n    // bound (some graph wrappers do), bindToolsIfSupported is a no-op.\n    const bound = bindToolsIfSupported(model, tools);\n\n    // Resolve flush prompts with the scope-aware rubric. For an\n    // isolated agent (no userId in scope), the user-tier paths are\n    // filtered out of the rubric so the LLM never sees them — it\n    // physically cannot route a write to `memory/user/*` because the\n    // prompt never lists those paths.\n    const { systemPrompt, prompt } = resolveFlushPrompts({\n      scope: memory.scope,\n    });\n    const initialMessages = [\n      new SystemMessage(systemPrompt),\n      new HumanMessage(\n        `${prompt}\\n\\nConversation context:\\n\\n${conversationSummary}`\n      ),\n    ];\n\n    const loop: FlushLoopResult = await runFlushLoop({\n      model: bound,\n      tools,\n      initialMessages,\n      maxIterations: maxIterations ?? DEFAULT_MAX_FLUSH_ITERATIONS,\n      onIteration: onIteration\n        ? (ev): void =>\n            onIteration({\n              i: ev.i,\n              toolCallCount: ev.toolCalls.length,\n              toolNames: ev.toolCalls.map((c) => c.name),\n            })\n        : undefined,\n    });\n\n    return {\n      ran: true,\n      iterations: loop.iterations,\n      appendsAttempted: loop.appendsAttempted,\n      appendsSucceeded: loop.appendsSucceeded,\n      toolErrors: loop.toolErrors,\n      silentReply: loop.silentReply,\n      hitIterationCap: loop.hitIterationCap,\n      finalText: loop.finalText,\n    };\n  } catch (err) {\n    return {\n      ran: false,\n      error: err instanceof Error ? err.message : String(err),\n    };\n  } finally {\n    setPhase(MEMORY_PHASE_NORMAL);\n  }\n}\n"],"names":[],"mappings":"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"}