Deep Agents
AgentContextOrchestratorRetrievalText2SQLToolbox

Reminders

Inject situational context into model-visible carriers so the LLM stays aware of hidden runtime facts, active skills, and recent conversation signals

Reminders inject hidden text into the model conversation to give the LLM situational awareness — current date, active skills, recent tool behavior, recurring instructions. Depending on the target, the text either merges into a real user message or becomes a persisted synthetic user message between model steps. Reminder text is model-facing, not user-facing.

How Reminders Work

A reminder is always a context fragment. Create it with reminder() and declare it on the engine with engine.set(); the engine folds it into the model's view when its when predicate fires. Two decisions control where and how:

  1. Wheretarget: 'user' (the default), target: 'tool-output', or target: 'steer'
  2. How — inline (appended as a <system-reminder> tag inside the last text part) or as a separate text part

The when predicate is optional for target: 'user' — omit it for an always-on instruction. For tool-output and steer a when predicate is required.

import { everyNTurns, once, reminder } from '@deepagents/context';

engine.set(reminder('Keep responses concise')); // user, always on
engine.set(reminder('Welcome!', { when: once('welcome') })); // user, one-time
engine.set(reminder('RECAP', { when: everyNTurns(3), target: 'steer' }));

User Reminders

target: 'user' is the default. The engine folds the reminder text into the last pending user message during engine.save(), baking it as a <system-reminder> tag (or its own text part). The folded text is persisted in the chain, so reloading reproduces exactly what the model saw.

Always-On Reminders

Omit when for a reminder that folds into every user message — a recurring instruction the model should always see.

import { reminder, user } from '@deepagents/context';

engine.set(reminder('Keep responses under 100 words'), user('Hello'));
await engine.save();
// The saved user message carries: Hello<system-reminder>Keep responses under 100 words</system-reminder>

The text may also be a (ctx) => string factory that receives the message content. A factory that returns an empty string skips injection entirely — useful for inline conditional logic without a predicate.

engine.set(
  reminder((ctx) =>
    ctx.content.includes('prod')
      ? 'Ask for confirmation before destructive actions'
      : '',
  ),
  user('deploy to prod'),
);

Conditional User Reminders

Add a when predicate to fold the reminder only when it fires — gated by turn count, message content, assistant/tool history, token usage, or time. The engine builds a WhenContext at save() time and evaluates each predicate.

import {
  afterTurn,
  and,
  contentIncludes,
  reminder,
  user,
} from '@deepagents/context';

// Only fires when the user mentions code AND after the first 2 turns
engine.set(
  reminder('Include code examples in your response', {
    when: and(contentIncludes(['code', 'example', 'snippet']), afterTurn(2)),
  }),
  user('Show me how to use async/await'),
);

Pass once(id) as the predicate for a one-time user reminder. The id is durably latched in the folded message's metadata, so a fresh engine re-reads it and the reminder never fires twice — even across restarts.

import { once, reminder, user } from '@deepagents/context';

engine.set(
  reminder('Welcome! Type /help to see commands', { when: once('welcome') }),
  user('hi'),
);

See Predicates for the full predicate catalog and composition rules.

Inline vs Part Mode

User reminders render in one of two modes:

  • Inline — appended as <system-reminder> tags to the last text part (default).
  • Part — added as a separate text part on the user message (asPart: true).

Reminders default inline so the model sees the context next to the user's words. Set asPart: true when structured content needs to stay in a standalone text part:

import { reminder, workflow } from '@deepagents/context';

engine.set(reminder('hint')); // inline
engine.set(reminder(workflow({ task: 'Deploy', steps: ['…'] }))); // inline
engine.set(
  reminder(workflow({ task: 'Deploy', steps: ['…'] }), { asPart: true }),
); // forced part

Use part mode for structured data (dates, skill lists, fragments) to keep reminder text separate from the user's message. asPart only applies to target: 'user'; tool-output and steer ignore it.

Reminder Targets

A reminder fires in one of three places, chosen by target:

TargetWhen it runsWhat the model sees
userDuring engine.save() on the last pending user messageReminder text folded into that user message
tool-outputAt the next prepareStep boundary, after a tool has a terminal outcome and before the next model generationThe raw tool result followed by a synthetic user message containing the reminder
steerAt an eligible prepareStep boundary after the model has already produced a content stepA synthetic user message containing one or more <system-reminder> parts

user is the default and may omit when. tool-output and steer require a when predicate — reminder() throws if it is missing.

During tool-output evaluation, the predicate context includes a discriminated ctx.toolOutcome. The engine evaluates every terminal state and lets the predicate decide which ones matter:

type ToolOutcome =
  | {
      state: 'output-available';
      name: string;
      input: unknown;
      output: unknown;
    }
  | {
      state: 'output-error';
      name: string;
      input: unknown;
      error: unknown;
      errorText: string;
    }
  | {
      state: 'output-denied';
      name: string;
      input: unknown;
      reason?: string;
    };

Use toolOutput(...) for normal state/name/input/output matching. Read ctx.toolOutcome directly when the condition needs more custom logic. lastAssistantMessage and lastAssistantMessages remain history fields; they do not represent the terminal outcome currently being evaluated.

import {
  elapsedExceeds,
  everyNTurns,
  reminder,
  toolOutput,
} from '@deepagents/context';

engine.set(
  reminder('Double-check filesystem side effects', {
    when: toolOutput({
      name: 'bash',
      state: 'output-available',
      input: (input) =>
        typeof input === 'object' &&
        input !== null &&
        'command' in input &&
        String(input.command).startsWith('rm '),
    }),
    target: 'tool-output',
  }),
  reminder('Explain why the tool could not run', {
    when: toolOutput({ state: 'output-denied' }),
    target: 'tool-output',
  }),
  reminder('Checkpoint before taking another tool step', {
    when: elapsedExceeds(40 * 60_000),
    target: 'steer',
  }),
  reminder('Cite sources every third turn', {
    when: everyNTurns(3),
  }),
);

Use tool-output when the reminder should follow a terminal tool outcome. Raw tool outputs are never wrapped or replaced. prepareStep appends a synthetic user message after the tool result, and AI SDK v7 carries that message override into later steps. Provider adapters may serialize adjacent provider-neutral messages into their native compound wire format; the runtime does not contain provider-specific reminder handling.

Use steer when the agent needs a hidden nudge during a multi-step turn without waiting for the next user message. Steer reminders do not run before the initial generation: context needed there belongs on a user reminder. A predicate that stays true fires at every eligible continuation boundary; compose once(id) when it should latch after the first fire.

A steer predicate is evaluated against the chain as it stands at that boundary, so it observes what the model did earlier in the same turn — a tool it called at step 3, a streak of failures, a segment count. That is what makes predicates like toolCallCount('bash', { gte: 2 }) or toolFailedStreak('bash', { gte: 3 }) usable as steer triggers.

For streamed chat turns, the engine persists the same assistant/tool result → synthetic user message → assistant split the model saw. Later top-level requests replay that exact prefix, preserving provider prompt cache stability. Tool-output and steer reminders that fire at the same boundary are merged into one synthetic user message.

once(id) works on user, tool-output, and steer. Its id is persisted only when the enclosing reminder actually fires, and the latch survives engine restarts.

Built-in Reminders

The library ships with pre-built reminders for common needs:

ReminderContext

A reminder's text factory receives a context object:

interface ReminderContext {
  content: string; // Plain text of the user message
  turn?: number; // Current user turn count
  lastMessageAt?: number; // Timestamp of last persisted user message
  lastMessage?: UIMessage; // Last persisted user message (with metadata)
  currentMessage?: UIMessage; // Pending user message for the current turn
  chat?: StoredChatData; // Active chat record
  usage?: LanguageModelUsage; // Accumulated usage metadata
  branch?: string; // Active branch name
  elapsed?: number; // Milliseconds since last persisted user message
  messageCount?: number; // Total message count across all roles
  lastAssistantMessage?: UIMessage; // Current assistant segment (see below)
  lastAssistantMessages?: UIMessage[]; // Every assistant segment in the chain
  lastAssistantReplies?: UIMessage[]; // One entry per reply, segments merged back
  toolOutcome?: ToolOutcome; // Current terminal outcome for tool-output
}

Derive counters from lastAssistantMessages, not lastAssistantMessage. When a steer or tool-output reminder fires, the engine carves the assistant message at that boundary (this is what preserves the prompt prefix — see Reminder Targets), so lastAssistantMessage afterwards holds only the parts produced since the last fire. A "this tool failed N times in a row" counter read from it silently resets to 1 on every fire. lastAssistantMessages keeps every segment, so it counts correctly — that is what toolFailureStreak() reads.

The engine populates these fields when it resolves reminder text. During tool-output evaluation, both the predicate and a dynamic text factory can read toolOutcome. A reminder gated by a when predicate receives the richer WhenContext (see Predicates); a plain (ctx) => string factory on a user reminder receives at least content.

Stripping Reminders

User reminders carry range metadata so they can be stripped. Tool-output and steer reminders use a whole-message synthetic marker:

type SyntheticReminderMetadata = {
  source: 'reminder';
  firedAt: number;
  onceIds?: string[];
};

Use isSyntheticReminderMessage() to omit these model-only messages from a UI. Use stripReminders() to remove reminder content and metadata when exporting or sanitizing individual messages.

import {
  isSyntheticReminderMessage,
  stripReminders,
} from '@deepagents/context';

const visibleMessages = messages
  .filter((message) => !isSyntheticReminderMessage(message))
  .map(stripReminders);

synthesizeReminderMessage(text, firedAt, onceIds?) builds the same synthetic user message for advanced host integrations. Normal agent() + chat() users do not need to call it: the agent creates the messages and chat() persists the matching assistant/reminder split. Direct AI SDK integrations that call createPrepareStep() own assistant persistence and must pair the hook with writeAssistantSegment() if they need prompt/store parity.