On-device models have small context windows. Apple Foundation Models has 4096 tokens on iOS 26, and llama.rn models default to 2048. The AI SDK sends the whole message list with every request, so a long chat eventually stops fitting.
wrapLanguageModelWithHistory from @react-native-ai/utils wraps any AI SDK
language model and compacts the prompt before each request. It can keep a
rolling window of recent messages, fold older messages into a summary, and drop
tool calls of earlier turns. Your chat state is not changed: only the prompt
sent to the model is.
@react-native-ai/utils is not on npm yet. Until it is, use it from this
monorepo the way the example app does, or copy
packages/utils/src/index.ts
into your app. Its only dependency is @ai-sdk/provider.
Each option works on its own, and you can combine them. They run in the order below.
dropCompletedToolCallsRemoves tool calls and tool results from turns before the latest user message. Assistant text from those turns is kept. Tool steps of the turn being answered are never touched, so multi-step tool calling keeps working.
rollingWindowMessagesKeeps system messages and the last N other messages. The window never cuts into the turn being answered: when the latest user message and its tool steps are longer than N messages, all of them are kept. The window also never starts with a tool result whose tool call fell outside it.
Without summarizeHistory, messages outside the window are dropped. With it,
they are summarized.
summarizeHistorymodel: the model that writes the summary. It can be the model you are
wrapping or a different one. Pass the unwrapped model.threshold: prompt size in tokens. When the prompt left after the rolling
window is larger, everything before the latest user message is summarized
too.The summary is added to the end of the last system message, or sent as a new system message when there is none:
Summaries are built incrementally. The wrapper remembers the last summary for each summarizer model and reuses it while the summarized messages are still the start of the history:
Summarizing a long history at once could overflow the summarizer's own context, so messages are folded in chunks that fit the threshold. Each summary is capped at 400 output tokens.
The summarizer call receives the request's abortSignal. If it throws, the
request fails with that error. If it returns no text, older messages are dropped
without a summary.
The threshold is compared to a token count of the system messages and
conversation. Models with a countTokens method count with their own
tokenizer, which Apple and llama.rn language models have. Other models use an
estimate of four characters per token.
The count does not include tool definitions, structured output schemas, or the summary itself, and the model still needs room for its answer. Start at about 60% of the context size:
contextSize from apple.getModelInfo(). Token counting needs iOS
26.4, and older versions fall back to the estimate.n_ctx you pass in contextParams.If the latest user message and its tool steps alone exceed the threshold, they are sent as they are.
Summarizing is an extra model call that runs before the request. On device it
adds latency to the turns that summarize. A rolling window with summarizeHistory summarizes on most
turns once the chat is longer than the window. summarizeHistory alone only
summarizes once the prompt passes the threshold.
Summaries lose detail. Keep the window large enough for the recent context the model needs word for word.
applyHistoryManagement(prompt, options, abortSignal?) returns the compacted
prompt without wrapping a model.trimMessagesForHistory(prompt, options) applies dropCompletedToolCalls and
rollingWindowMessages synchronously, without summarizing.Both take an AI SDK LanguageModelV4Prompt, the format providers receive.