flutter_llm_memory

A memory layer that decides which conversation messages to send to an LLM, so requests stay within the context window and cost stays down.

The problem

LLMs are stateless. To keep a conversation coherent you resend the history on every request. As the conversation grows, each request gets larger, costs rise, and eventually you hit the model's context limit and the request fails. This package sits between your chat code and your LLM client and manages what gets sent: it tracks history, estimates tokens, prunes or summarizes old messages, and reports how many tokens you saved.

Install

dependencies:
  flutter_llm_memory: ^0.1.0
dart pub add flutter_llm_memory

Quick start

import 'package:flutter_llm_memory/flutter_llm_memory.dart';

final memory = ConversationMemory(
  strategy: SlidingWindowStrategy(maxTokens: 4000),
);

await memory.add(Message.user('Hello, I need help with my order'));
await memory.add(Message.assistant('Sure, what is your order number?'));
await memory.add(Message.user('It is 48213'));

// Build the messages to send to your LLM.
final (context, stats) = await memory.buildContext();

// `context` is a List<Message>. Map it to your provider's request shape.
final payload = context
    .map((message) => {'role': message.role.name, 'content': message.content})
    .toList();

print('Sending ${stats.currentTokens} tokens, saved ${stats.tokensSaved}');

The package never calls an LLM. You map context to your provider and make the request yourself.

Strategies

Strategy Best for LLM calls needed
SlidingWindowStrategy Short sessions, no summarization No
SummarizationStrategy Long sessions, fresh summary each time Yes
HybridMemoryStrategy Long sessions, incremental memory Yes

SlidingWindowStrategy

Keeps the most recent messages that fit in the budget. System messages are always kept.

final strategy = SlidingWindowStrategy(maxTokens: 4000);

SummarizationStrategy

Once the conversation exceeds the budget, older messages are summarized by your function and replaced with a single summary message. The most recent keepRecentCount messages stay in full.

final strategy = SummarizationStrategy(
  maxTokens: 4000,
  keepRecentCount: 10,
  summarizer: (olderMessages) => summarizeWithMyLlm(olderMessages),
);

HybridMemoryStrategy

Keeps a running summary that grows incrementally plus a window of recent messages. It summarizes only the oldest overflow messages per event, so it makes one summarization call per overflow instead of resummarizing everything.

final strategy = HybridMemoryStrategy(
  maxTokens: 4000,
  keepRecentCount: 8,
  summarizeChunkSize: 4,
  summarizer: (olderMessages) => summarizeWithMyLlm(olderMessages),
);

If your summarizer throws, all three summarizing strategies fall back to sliding window behavior for that call instead of crashing.

Persistence

History is in memory by default. Provide a MemoryStorage adapter to persist it, then restore on startup.

final memory = await ConversationMemory.restore(
  storage: SharedPreferencesStorage(),
  strategy: HybridMemoryStrategy(
    maxTokens: 4000,
    summarizer: summarizeWithMyLlm,
  ),
);

MemoryStorage is a three-method interface (save, load, clear). The package ships InMemoryStorage. A SharedPreferencesStorage reference implementation lives in the example app; copy it or adapt it for Hive or SQLite.

Stats

Every buildContext() returns a MemoryStats, also available as memory.stats after the call.

final (context, stats) = await memory.buildContext();

stats.currentTokens;        // tokens in the context that will be sent
stats.totalTokens;          // tokens in the full untrimmed history
stats.tokensSaved;          // totalTokens - currentTokens
stats.messageCount;         // messages in the full history
stats.contextMessageCount;  // messages in the returned context
stats.compressionRatio;     // currentTokens / totalTokens, 1.0 means no compression
stats.isSummarized;         // whether a summary block is present

Bring your own LLM

This package does not depend on any provider and never makes a network call. You pass a summarizer and you send the built context yourself. Here is a summarizer backed by OpenAI:

Future<String> summarizeWithMyLlm(List<Message> messages) async {
  final transcript =
      messages.map((m) => '${m.role.name}: ${m.content}').join('\n');
  final response = await http.post(
    Uri.parse('https://api.openai.com/v1/chat/completions'),
    headers: {
      'Content-Type': 'application/json',
      'Authorization': 'Bearer $openAiKey',
    },
    body: jsonEncode({
      'model': 'gpt-4o-mini',
      'messages': [
        {'role': 'system', 'content': 'Summarize this conversation concisely.'},
        {'role': 'user', 'content': transcript},
      ],
    }),
  );
  final data = jsonDecode(response.body) as Map<String, dynamic>;
  return data['choices'][0]['message']['content'] as String;
}

Model token limits reference

Model Context window
GPT-4o 128k
Claude Sonnet 200k
Gemini 1.5 Pro 1M
Llama 3 70B 8k

Set maxTokens to about 80% of the model limit to leave room for the response.

Contributing

PRs are welcome. For anything larger than a bug fix, open an issue first so we can agree on the approach before you write code.

License

MIT. See LICENSE.

Libraries

flutter_llm_memory
An intelligent memory layer between a Flutter/Dart chat UI and any LLM provider.