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 { everyNTurns, reminder, 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[];
lastAssistantReplies?: UIMessage[];
toolOutcome?: ToolOutcome;
firedOnceIds?: ReadonlySet<string>;
onceCollector?: Set<string>;
}| 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 real user UIMessage for the current turn |
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 current assistant segment — only the parts produced since the last reminder fired (see below) |
lastAssistantMessages | Every assistant segment in the chain; read this for any history-derived counter (e.g. a tool-failure streak) |
lastAssistantReplies | One entry per assistant reply, with its carved segments merged back; what the window combinators count |
toolOutcome | The terminal outcome currently being evaluated for a tool-output reminder; undefined for user and steer reminders |
firedOnceIds | Ids already latched by once() this conversation across all reminder targets |
onceCollector | Per-evaluation buffer once() appends to; the engine commits it only if the reminder fires |
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. toolOutcome describes the terminal tool result being considered before the next model generation; lastAssistantMessage and lastAssistantMessages describe assistant history. The firedOnceIds/onceCollector fields back the once() latch and are managed by the engine — predicates compose once(id) rather than reading them directly.
Predicates are evaluated against the chain as it stands at each boundary, so a steer or tool-output predicate sees what the model did earlier in the same turn.
When a steer or tool-output reminder fires, the engine carves the assistant
message at that boundary to preserve the prompt prefix. lastAssistantMessage
therefore holds only the parts produced since the last fire — a counter derived
from it silently resets to 1 every time the reminder fires. Derive counters from
lastAssistantMessages, which keeps every segment. toolFailureStreak() below
does exactly this.
Turn-Based Predicates
everyNTurns(n)
Fires when the turn count is divisible by n.
everyNTurns(3); // fires on turns 3, 6, 9, 12, ...first()
Fires only on the first turn.
first(); // 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 { contentMatches, reminder } 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 { BM25Classifier, classifies, reminder } 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.
Current Tool-Outcome Predicate
toolOutput(options?)
Matches the terminal tool outcome currently being evaluated by a
target: 'tool-output' reminder. It does not search assistant history.
import { reminder, toolOutput } from '@deepagents/context';
engine.set(
reminder('Recover from the failed command', {
target: 'tool-output',
when: toolOutput({
name: 'bash',
state: 'output-error',
errorText: (text) => text.includes('permission denied'),
}),
}),
);| Option | Type | Description |
|---|---|---|
name | string | ((name: string) => boolean) | Match the current tool name |
state | 'output-available' | 'output-error' | 'output-denied' | Restrict matching to one terminal state |
input | (input: unknown) => boolean | Inspect the tool input in any state |
output | (output: unknown) => boolean | Inspect a successful raw output |
error | (error: unknown) => boolean | Inspect the raw thrown or validation error |
errorText | (text: string) => boolean | Inspect the normalized error text sent to the model |
reason | (reason: string | undefined) => boolean | Inspect an execution-denial reason |
The engine evaluates successful results, thrown errors, invalid-input errors, approval denials, and provider-executed terminal outcomes. Preliminary streaming tool outputs are not terminal and are not evaluated.
Assistant-History Tool Predicates
These predicates inspect tool parts on lastAssistantMessage, so they describe completed tool calls already in conversation history. Use toolOutput() instead when a tool-output reminder needs the terminal outcome at the current step boundary.
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 });Counts calls in the current assistant segment. To count across a whole turn that a firing reminder has carved into segments, use toolFailedStreak below or read ctx.lastAssistantMessages directly.
toolFailedStreak(name, spec)
Gates on a run of consecutive failures of name, counting back from the newest result. A success ends the run, so the streak resets on its own.
import { reminder, toolFailedStreak } from '@deepagents/context';
engine.set(
reminder('`bash` keeps failing — change the command, do not retry it.', {
when: toolFailedStreak('bash', { gte: 3 }),
target: 'tool-output',
}),
);toolFailureStreak(ctx, name)
The number behind toolFailedStreak — call it from a text factory to escalate the wording as the run grows. Both walk lastAssistantMessages, so they keep counting across the segments a firing reminder creates.
import { reminder, toolFailedStreak, toolFailureStreak } from '@deepagents/context';
engine.set(
reminder(
(ctx) => {
const failures = toolFailureStreak(ctx, 'bash');
return failures > 3
? `\`bash\` has failed ${failures} times in a row. Stop and explain the blocker.`
: `\`bash\` failed ${failures} times in a row. Change the command.`;
},
{ when: toolFailedStreak('bash', { gte: 1 }), target: 'tool-output' },
),
);Durable Fire-Once Predicate
once(id)
Returns true until id has fired, then remains false for the conversation. The
engine stores the id only if the complete reminder predicate fires, so it is
safe inside and() and or().
import { and, once, reminder, toolOutput } from '@deepagents/context';
engine.set(
reminder('Explain the first tool failure', {
target: 'tool-output',
when: and(toolOutput({ state: 'output-error' }), once('first-tool-error')),
}),
);The latch works for user, tool-output, and steer reminders and survives
engine restarts.
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.
n counts replies, not stored assistant messages. One reply is everything the assistant produced in answer to a single user message — a firing steer/tool-output reminder carves that reply into several stored messages, and these combinators merge them back before matching. So registering an unrelated reminder never changes what n means.
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 { afterTurn, and, dayChanged } from '@deepagents/context';
and(dayChanged(), afterTurn(3));
// day changed AND past turn 3or(...predicates)
Any predicate must return true.
import { dayChanged, hourChanged, or } from '@deepagents/context';
or(dayChanged(), hourChanged());
// day OR hour changednot(predicate)
Negates a predicate.
import { dayChanged, not } from '@deepagents/context';
not(dayChanged());
// day has NOT changedNesting
Combinators nest freely:
and(or(dayChanged(), hourChanged()), not(first()), afterTurn(2));Custom Predicates
A predicate is any function with the signature (ctx: WhenContext) => boolean | Promise<boolean>:
import { type WhenPredicate, reminder } 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 { contentIncludes, hint, reminder, workflow } 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'),
);
// Always-on fragment reminder (no `when`): a user-target reminder with no
// predicate fires on every turn.
engine.set(
reminder(workflow({ task: 'Greet', steps: ['Say hi'] })),
user('hello'),
);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 },
),
);