Deep Agents
AgentContextOrchestratorRetrievalText2SQLToolbox

Predicates

Composable functions that control when conditional reminders fire based on turns, content, assistant history, tool usage, token usage, or time

A predicate is a function that receives a WhenContext and returns true (fire the reminder) or false (skip it). Pass predicates to reminder() via the when option to create conditional reminders.

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

engine.set(
  reminder('Stay concise', { when: everyNTurns(3) }),
  user('hello'),
);

WhenContext

Every predicate receives this context object:

interface WhenContext {
  turn: number;
  content: string;
  lastMessageAt?: number;
  lastMessage?: UIMessage;
  currentMessage?: UIMessage;
  chat: StoredChatData;
  usage?: LanguageModelUsage;
  branch: string;
  elapsed?: number;
  messageCount: number;
  lastAssistantMessage?: UIMessage;
  lastAssistantMessages?: UIMessage[];
}
FieldDescription
turnCurrent user turn count (persisted + pending)
contentPlain text of the user message
lastMessageAtTimestamp of the last persisted user message
lastMessageThe last persisted user UIMessage (with metadata)
currentMessageThe pending user UIMessage for the current turn (visible before persistence)
chatFull StoredChatData object (id, userId, metadata, timestamps)
usageAccumulated LanguageModelUsage from chat.metadata.usage
branchActive branch name (e.g. "main")
elapsedMilliseconds since the last persisted user message
messageCountTotal message count across all roles
lastAssistantMessageThe last persisted assistant UIMessage
lastAssistantMessagesAssistant-message history for windowed predicates such as withinLastN()

currentMessage lets a predicate read metadata the user attached to this turn — e.g., currentMessage.metadata.locale.timeZone set on turn 1, before any prior lastMessage exists.

Turn-Based Predicates

everyNTurns(n)

Fires when the turn count is divisible by n.

everyNTurns(3); // fires on turns 3, 6, 9, 12, ...

once()

Fires only on the first turn.

once(); // fires on turn 1 only

firstN(n)

Fires on turns 1 through n (inclusive).

firstN(5); // fires on turns 1, 2, 3, 4, 5

afterTurn(n)

Fires after turn n (exclusive).

afterTurn(2); // fires on turns 3, 4, 5, ...

Content-Based Predicates

contentIncludes(keywords)

Fires when the message contains any of the given keywords (case-insensitive).

contentIncludes(['deploy', 'release', 'ship']);
// fires when user says "let's deploy" or "ship it"

contentPattern(pattern)

Fires when the message matches a regular expression.

contentPattern(/\b(SELECT|INSERT|UPDATE|DELETE)\b/i);
// fires when user includes SQL keywords

Content Matching Predicates

These predicates use BM25 text matching to determine relevance, offering more sophisticated matching than keyword or regex checks.

contentMatches(topics, options?)

Fires when the user's message relates to any of the given topics, using BM25 scoring internally. Each topic string becomes a document in the corpus, and the user's message is scored against them.

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

engine.set(
  reminder('Consider indexing strategies', {
    when: contentMatches(['database optimization', 'query performance']),
  }),
);
OptionTypeDefaultDescription
thresholdnumber0Minimum BM25 score to count as a match

classifies(classifier, options?)

Fires when the given classifier returns at least one result for the user's message. Works with any IClassifier<T> implementation, not just the built-in BM25Classifier.

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

const classifier = new BM25Classifier([
  { name: 'auth', description: 'authentication and authorization' },
  { name: 'data', description: 'database and data modeling' },
]);

engine.set(
  reminder('Check related skills', {
    when: classifies(classifier, { threshold: 0.1 }),
  }),
);
OptionTypeDefaultDescription
topNnumber5Maximum number of results the classifier returns
thresholdnumber0Minimum score to count as a match

See Skill Reminders for details on IClassifier, BM25Classifier, and building custom classifiers.

Assistant-History Predicates

lastAssistantLength(spec)

Fires when the plain-text length of the last persisted assistant reply matches a count spec.

import { lastAssistantLength } from '@deepagents/context';

lastAssistantLength({ gte: 2_000 }); // assistant reply is at least 2,000 chars
lastAssistantLength({ eq: 0 });      // assistant reply is empty

spec uses this shape:

type CountSpec = { gte?: number; lte?: number; eq?: number };

Use either eq or a gte/lte range. eq cannot be combined with range bounds.

Tool-Based Predicates

These predicates inspect tool parts on lastAssistantMessage.

toolCalled(name)

Fires when the last assistant reply includes at least one completed tool call whose tool name matches name.

import { toolCalled } from '@deepagents/context';

toolCalled('bash');
toolCalled((name) => name.startsWith('http_'));

toolFailed(name)

Fires when the last assistant reply includes a matching tool call with state output-error.

import { toolFailed } from '@deepagents/context';

toolFailed('bash');

anyToolCalled()

Fires when the last assistant reply includes any completed tool call.

import { anyToolCalled } from '@deepagents/context';

anyToolCalled();

toolCall(options)

Low-level matcher for tool parts. If you omit state, it only considers completed tool parts: input-available, output-available, and output-error.

import { toolCall } from '@deepagents/context';

toolCall({
  name: 'bash',
  state: 'output-error',
  errorText: (text) => text.includes('permission denied'),
});
OptionTypeDescription
namestring | ((name: string) => boolean)Match a tool name exactly or with a predicate
stateToolUIPart['state']Restrict matching to a specific tool part state
input(input: unknown) => booleanInspect the tool input payload
output(output: unknown) => booleanInspect the output payload for output-available parts
errorText(text: string) => booleanInspect the error text for output-error parts

toolCallCount(name, spec)

Counts completed tool calls in the last assistant reply whose name matches name, then applies a CountSpec.

import { toolCallCount } from '@deepagents/context';

toolCallCount('bash', { gte: 3 });
toolCallCount('sql', { eq: 1 });

Usage and Elapsed Predicates

usageExceeds(totalTokens)

Fires when accumulated chat.metadata.usage.totalTokens is greater than or equal to totalTokens.

import { usageExceeds } from '@deepagents/context';

usageExceeds(10_000);

elapsedExceeds(ms)

Fires when the elapsed time since the last persisted user message is greater than or equal to ms.

import { elapsedExceeds } from '@deepagents/context';

elapsedExceeds(60_000); // one minute

Temporal Predicates

Compare new Date() against lastMessageAt using timezone-aware formatting. All return true on the first turn (when lastMessageAt is undefined).

PredicateFires when...
dayChanged(options?)The calendar date has changed
hourChanged(options?)The hour has changed
monthChanged(options?)The month has changed
yearChanged(options?)The year has changed
seasonChanged(options?)The meteorological season has changed
weekChanged(options?)The ISO week has changed

Each accepts an optional { tz?: string }. When tz is omitted, the predicate falls back to currentMessage.metadata.locale.timeZone, then lastMessage.metadata.locale.timeZone, then 'UTC'.

import { dayChanged, hourChanged } from '@deepagents/context';

dayChanged();                          // resolves tz from message metadata, else UTC
dayChanged({ tz: 'America/New_York' }); // explicit Eastern time
hourChanged({ tz: 'Asia/Tokyo' });     // explicit JST

Pair tz-less predicates with localeReminder (or any user message that sets metadata.locale) and they pick the user's timezone up automatically:

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

engine.set(
  localeReminder({ language: 'Japanese', timeZone: 'Asia/Tokyo' }),
  reminder('Day rolled over', { when: dayChanged() }),
  user('hello'),
);

See Temporal Reminders → Timezone resolution for the full lookup order, including how to make turn-1 metadata visible.

Seasons follow meteorological boundaries: Winter (Dec-Feb), Spring (Mar-May), Summer (Jun-Aug), Fall (Sep-Nov).

History Window Combinators

These helpers scan multiple assistant replies by rebinding only lastAssistantMessage. Fields such as content, turn, usage, and elapsed stay fixed from the outer WhenContext.

withinLastN(n, predicate)

Fires when predicate matches at least one of the last n assistant replies.

import { toolFailed, withinLastN } from '@deepagents/context';

withinLastN(5, toolFailed('bash'));

everyOfLastN(n, predicate)

Fires only when predicate matches every one of the last n assistant replies.

import { anyToolCalled, everyOfLastN, not } from '@deepagents/context';

everyOfLastN(3, not(anyToolCalled()));

These pair best with toolCalled(), toolCall(), toolCallCount(), anyToolCalled(), toolFailed(), and lastAssistantLength().

Boolean Combinators

Compose predicates with boolean logic.

and(...predicates)

All predicates must return true.

import { and, dayChanged, afterTurn } from '@deepagents/context';

and(dayChanged(), afterTurn(3));
// day changed AND past turn 3

or(...predicates)

Any predicate must return true.

import { or, dayChanged, hourChanged } from '@deepagents/context';

or(dayChanged(), hourChanged());
// day OR hour changed

not(predicate)

Negates a predicate.

import { not, dayChanged } from '@deepagents/context';

not(dayChanged());
// day has NOT changed

Nesting

Combinators nest freely:

and(
  or(dayChanged(), hourChanged()),
  not(once()),
  afterTurn(2),
);

Custom Predicates

A predicate is any function with the signature (ctx: WhenContext) => boolean | Promise<boolean>:

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

const businessHours: WhenPredicate = (ctx) => {
  const hour = new Date().getHours();
  return hour >= 9 && hour < 17;
};

engine.set(
  reminder('User is likely at work — keep responses professional', {
    when: businessHours,
  }),
);

Predicates can be async for cases like external API checks or LLM classifiers:

const isHighPriority: WhenPredicate = async (ctx) => {
  const priority = await classifyPriority(ctx.content);
  return priority === 'high';
};

Fragment Composition

reminder() accepts any ContextFragment as its first argument. The fragment is pre-rendered to XML and injected as reminder text when the predicate fires.

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

// Inject a workflow as a reminder when the user mentions errors
engine.set(
  reminder(
    workflow({
      task: 'Error recovery',
      steps: ['Read error message', 'Check schema', 'Fix query'],
    }),
    { when: contentIncludes(['error', 'fail']) },
  ),
  user('my query failed'),
);

// Works with any fragment type
engine.set(
  reminder(hint('Check indexes for slow queries'), { when: contentIncludes(['slow']) }),
  user('this query is slow'),
);

// Immediate fragment reminders (no predicate)
engine.set(
  user('hello', reminder(workflow({ task: 'Greet', steps: ['Say hi'] }))),
);

Factory With Context

Combine a custom predicate with a factory reminder to use both WhenContext (for gating) and ReminderContext (for dynamic text):

engine.set(
  reminder(
    (ctx) => {
      if (!ctx.lastMessageAt) return 'Session started.';
      const hours = Math.floor((Date.now() - ctx.lastMessageAt) / 3600000);
      return `${hours}h since last message.`;
    },
    { when: dayChanged(), asPart: false },
  ),
);