/** * `memory_append` — the reflection-phase-only write tool. * * New (not present in upstream verbatim). Upstream enforces append-only via * `wrapToolMemoryFlushAppendOnlyWrite`; we combine the tool and the phase * gate in one place because the agents library has a simpler graph state. * * Behaviour: * - Outside `memory_flushing` phase: returns an error object instead of * calling the backend. The LLM sees this and stops trying. * - Inside `memory_flushing`: persists the note via {@link MemoryBackend.append}. * - Hard-caps total appends per flush (protects against runaway writes). */ import { tool } from '@langchain/core/tools'; import { DEFAULT_MAX_APPENDS_PER_FLUSH, MEMORY_APPEND_DESCRIPTION, MEMORY_APPEND_TOOL_NAME, MEMORY_PHASE_FLUSHING, } from '@/memory/constants'; import type { MemoryPhase } from '@/memory/types'; import { MemoryAppendSchema, toAppendInput, type MemoryToolBinding, } from './shared'; export interface MemoryAppendBinding extends MemoryToolBinding { /** * Reads the current phase at call-time. Required — without it the append * tool rejects every call. Supplied by the graph runtime. */ getPhase: () => MemoryPhase; /** Hard cap on appends per flush phase. Default 20. */ maxAppendsPerFlush?: number; } interface AppendResult { ok: boolean; error?: string; path?: string; } // eslint-disable-next-line @typescript-eslint/explicit-function-return-type export function createMemoryAppendTool(binding: MemoryAppendBinding) { const maxAppends = binding.maxAppendsPerFlush ?? DEFAULT_MAX_APPENDS_PER_FLUSH; let appendsInCurrentFlush = 0; let lastSeenPhase: MemoryPhase = 'normal'; return tool( async (args): Promise => { const phase = binding.getPhase(); // Reset counter on entry to a new flush. if ( phase === MEMORY_PHASE_FLUSHING && lastSeenPhase !== MEMORY_PHASE_FLUSHING ) { appendsInCurrentFlush = 0; } lastSeenPhase = phase; if (phase !== MEMORY_PHASE_FLUSHING) { const result: AppendResult = { ok: false, error: 'memory_append can only be called during the reflection (memory_flushing) phase', }; return JSON.stringify(result); } if (appendsInCurrentFlush >= maxAppends) { const result: AppendResult = { ok: false, error: `memory_append hit the per-flush cap of ${maxAppends} notes`, }; return JSON.stringify(result); } try { const input = toAppendInput(args); await binding.backend.append(binding.scope, input); appendsInCurrentFlush += 1; const result: AppendResult = { ok: true, path: input.path }; return JSON.stringify(result); } catch (err) { const result: AppendResult = { ok: false, error: err instanceof Error ? err.message : String(err), }; return JSON.stringify(result); } }, { name: MEMORY_APPEND_TOOL_NAME, description: MEMORY_APPEND_DESCRIPTION, schema: MemoryAppendSchema, } ); }