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[];
}| Field | Description |
|---|---|
turn | Current user turn count (persisted + pending) |
content | Plain text of the user message |
lastMessageAt | Timestamp of the last persisted user message |
lastMessage | The last persisted user UIMessage (with metadata) |
currentMessage | The pending user UIMessage for the current turn (visible before persistence) |
chat | Full StoredChatData object (id, userId, metadata, timestamps) |
usage | Accumulated LanguageModelUsage from chat.metadata.usage |
branch | Active branch name (e.g. "main") |
elapsed | Milliseconds since the last persisted user message |
messageCount | Total message count across all roles |
lastAssistantMessage | The last persisted assistant UIMessage |
lastAssistantMessages | Assistant-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 onlyfirstN(n)
Fires on turns 1 through n (inclusive).
firstN(5); // fires on turns 1, 2, 3, 4, 5afterTurn(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 keywordsContent 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']),
}),
);| Option | Type | Default | Description |
|---|---|---|---|
threshold | number | 0 | Minimum 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 }),
}),
);| Option | Type | Default | Description |
|---|---|---|---|
topN | number | 5 | Maximum number of results the classifier returns |
threshold | number | 0 | Minimum 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 emptyspec 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'),
});| Option | Type | Description |
|---|---|---|
name | string | ((name: string) => boolean) | Match a tool name exactly or with a predicate |
state | ToolUIPart['state'] | Restrict matching to a specific tool part state |
input | (input: unknown) => boolean | Inspect the tool input payload |
output | (output: unknown) => boolean | Inspect the output payload for output-available parts |
errorText | (text: string) => boolean | Inspect 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 minuteTemporal Predicates
Compare new Date() against lastMessageAt using timezone-aware formatting. All return true on the first turn (when lastMessageAt is undefined).
| Predicate | Fires 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 JSTPair 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 3or(...predicates)
Any predicate must return true.
import { or, dayChanged, hourChanged } from '@deepagents/context';
or(dayChanged(), hourChanged());
// day OR hour changednot(predicate)
Negates a predicate.
import { not, dayChanged } from '@deepagents/context';
not(dayChanged());
// day has NOT changedNesting
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 },
),
);