IndexThinking.Sqlite
0.33.0
dotnet add package IndexThinking.Sqlite --version 0.33.0
NuGet\Install-Package IndexThinking.Sqlite -Version 0.33.0
<PackageReference Include="IndexThinking.Sqlite" Version="0.33.0" />
<PackageVersion Include="IndexThinking.Sqlite" Version="0.33.0" />
<PackageReference Include="IndexThinking.Sqlite" />
paket add IndexThinking.Sqlite --version 0.33.0
#r "nuget: IndexThinking.Sqlite, 0.33.0"
#:package IndexThinking.Sqlite@0.33.0
#addin nuget:?package=IndexThinking.Sqlite&version=0.33.0
#tool nuget:?package=IndexThinking.Sqlite&version=0.33.0
IndexThinking
Working Memory Manager for Reasoning-capable LLMs
What It Does
IndexThinking handles the repetitive-but-hard parts of LLM integration:
- Truncation Recovery - Auto-continue when responses hit token limits (
GetResponseAsync; a truncated stream is reported asWasTruncated) - Reasoning Extraction - Unified API for provider-specific thinking formats
- Token Management - Token usage tracking (
IBudgetTracker) and complexity estimation (IComplexityEstimator), reported in turn metrics — informational, not limits. To cap how much a model thinks, set it on the request:ChatOptions.Reasoning(effort) andChatOptions.MaxOutputTokens; IndexThinking extracts the reasoning, it does not throttle it - Content Recovery - Repair truncated JSON/code blocks
- Thinking State Stores -
IThinkingStateStoreimplementations: in-memory (AddIndexThinkingInMemoryStorage) and distributed-cache (AddIndexThinkingDistributedStorage) in the core package, persistent SQLite inIndexThinking.Sqlite. All are opt-in —AddIndexThinkingAgentsregisters no store, and without one no per-session state is saved
Features
Each row: what it does · entry point · how to turn it on. "Default" means the Quick Start registration (AddIndexThinkingAgents() + UseIndexThinking()) already enables it.
| Feature | Entry point | How to turn it on |
|---|---|---|
Turn pipeline over any IChatClient |
ChatClientBuilder.UseIndexThinking() → ThinkingChatClient |
Register AddIndexThinkingAgents() (required — resolves IThinkingTurnManager), then UseIndexThinking(o => …); or new ThinkingChatClient(inner, turnManager, options, …) without DI |
| Truncation detection + auto-continuation | ITruncationDetector, IContinuationHandler; limits in ThinkingChatClientOptions.DefaultContinuation (ContinuationConfig: MaxContinuations 5, MaxTotalDuration 5 min, …) |
Default on GetResponseAsync. A truncated stream is not continued — it is reported as TurnResult.WasTruncated |
| Truncated JSON / code-block repair | ContentRecoveryUtils (TryRecoverJson, TryRecoverCodeBlocks, CombineFragments) |
Default during continuation (ContinuationConfig.EnableJsonRecovery / EnableCodeBlockRecovery, both true); also callable directly |
Reasoning extraction (OpenAI, Anthropic, Gemini, <think> tags) |
IReasoningParser, ReasoningParserRegistry.Default; result via response.GetThinkingContent() |
Default (IncludeThinkingInMetadata = true) |
| Turn metrics | response.GetTurnMetrics() / GetTurnResult(); on a stream, the final update's AdditionalProperties[ThinkingChatClient.TurnResultKey] |
Default (IncludeMetricsInMetadata = true) |
| Reasoning activation for DeepSeek / Qwen / vLLM / GPUStack | ReasoningRequestModifierRegistry.Default |
Opt-in: ThinkingChatClientOptions.EnableReasoning = true (AutoDetectReasoningRequirement keeps it to models that need it). A custom registry from AddIndexThinkingReasoningModifiers(…) is what the DI UseIndexThinking() uses (or pass modifierRegistry: to the ThinkingChatClient constructor); registering it does not turn reasoning on by itself |
Live <think> separation in streams |
ThinkingChatClientOptions.SeparateReasoningInStream |
Opt-in (default false) — see below |
| Fit a request into a token budget | ContextBudgetChatReducer (ContextBudgetOptions: MaxInputTokens required, ToolResults = ToolResultTrimmer — 30 000 characters, first 50 and last 20 lines) — an M.E.AI IChatReducer: tool results over the limit keep their first and last lines; over the budget old tool results are masked (Masking — a ToolResultMasker; Retention lets the model read a masked one back by id; its minimumClearTokens masks only once enough has piled up past the boundary, so a prompt cache misses rarely), then the oldest messages leave, a tool round (call + results) always whole; system messages, the last user message and the newest round stay; a note marks each gap |
Opt-in: ChatClientBuilder.UseChatReducer(new ContextBudgetChatReducer(new() { MaxInputTokens = … })), registered after UseFunctionInvocation() so every round of a tool loop is fitted |
| Token counting | ITokenCounter / IChatMessageTokenCounter: ApproximateTokenCounter (registered by AddIndexThinkingAgents), TiktokenTokenCounter.ForModel(…), TokenCounterFactory |
Default approximate counter — within a few percent of o200k_base on prose, Korean, code and JSON; a message counts its text, tool calls and results, reasoning and images (ImageTokens each). Other tokenizers differ (cl100k needs ~1.5x more for Korean); for a local model pass ApproximateTokenCounter.ForModel(modelId) — ratios fitted to Gemma's or Qwen's tokenizer (LanguageRatios.Gemma / .Qwen, which TokenCounterFactory.Create(modelId) also picks), where the defaults undercount JSON by a fifth to a quarter. The client also counts the request's tool definitions and, by default, scales its estimates by what the provider reports (CalibrateTokenCounts, TokenEstimateFactor). Register your own ITokenCounter before AddIndexThinkingAgents() to replace it |
| Output-token boost and cap to the context window | ThinkingChatClientOptions.ThinkingOutputMultiplier (2, applied when reasoning is enabled), MaxContextTokens |
Default on GetResponseAsync when the request sets MaxOutputTokens and a token counter is available (the cap); not applied to streams |
| Budget / complexity reporting | IBudgetTracker, IComplexityEstimator |
Default; informational only (see What It Does) |
| Per-session thinking state | IThinkingStateStore |
Opt-in: AddIndexThinkingInMemoryStorage(), AddIndexThinkingDistributedStorage() (needs an IDistributedCache), or AddIndexThinkingSqliteStorage(connectionString) from IndexThinking.Sqlite |
| Store health check | ThinkingStateStoreHealthCheck |
Opt-in: AddIndexThinkingHealthChecks(), then services.AddHealthChecks().AddCheck<ThinkingStateStoreHealthCheck>("thinking-state-store") |
Metrics (meter IndexThinking: indexthinking.turns.total, .continuations.total, .truncations.total, .tokens.thinking, indexthinking.turn.duration) |
IndexThinkingMeter |
Opt-in: AddIndexThinkingMetrics() — the DI UseIndexThinking() records every turn on it (or pass meter: to the ThinkingChatClient constructor) |
Scope
IndexThinking shapes one model call — it fits what goes in to the context window and reads what comes out — not multi-step workflows.
| IndexThinking | Agent Orchestrators |
|---|---|
| Single turn optimization | Multi-step coordination |
| Building block | Workflow controller |
| Used BY orchestrators | Uses IndexThinking |
Token counting role boundary — IndexThinking owns token counting (ITokenCounter, framework-neutral; IChatMessageTokenCounter for M.E.AI ChatMessage counting). Model metadata (context window, pricing) belongs to TokenMeter; the consumer picks the budget from the window it knows, and ContextBudgetChatReducer fits a request into it.
Long-term memory boundary — IndexThinking keeps no long-term memory and calls no memory store. To give a turn recalled memories, recall them before the turn and add them to the messages you send.
Packages
| Package | Use |
|---|---|
IndexThinking |
Turn handling, reasoning extraction, token counting, in-memory and distributed-cache state stores |
IndexThinking.Sqlite |
Persistent SQLite IThinkingStateStore — services.AddIndexThinkingSqliteStorage("Data Source=thinking.db"). Carries Microsoft.Data.Sqlite and the native e_sqlite3; the core package does not |
Quick Start
dotnet add package IndexThinking
// Register services
services.AddIndexThinkingAgents();
// Wrap any IChatClient
var client = new ChatClientBuilder(innerClient)
.UseIndexThinking()
.Build(serviceProvider);
// Use normally
var response = await client.GetResponseAsync(messages);
// Access metadata
var thinking = response.GetThinkingContent();
var metrics = response.GetTurnMetrics();
Conversation History
The client is stateless: send the conversation you want the model to see, and keep it yourself (ChatMessage list, a store of
your own). To fit a long history into the window, register ContextBudgetChatReducer (see What It Does). A session id
(ThinkingChatClientExtensions.WithSession(id) or ChatOptions key IndexThinking.SessionId) only keys the thinking state
store and the per-session metrics.
Streaming with Thinking Orchestration
Streaming uses a Collect-and-Yield pattern: chunks are yielded to the caller immediately while buffered internally. After the stream completes, the buffered response is processed through the full orchestration pipeline (reasoning parsing, budget tracking). A stream cut off at the output limit is not continued — the caller already has every chunk — and is reported as TurnResult.WasTruncated. The output-token boost and the context-window cap of GetResponseAsync do not apply either: a streamed request is sent with the caller's own MaxOutputTokens.
await foreach (var update in client.GetStreamingResponseAsync(messages))
{
// Real-time chunks arrive here
Console.Write(update.Text);
// The final update contains orchestration metadata
if (update.AdditionalProperties?.ContainsKey(ThinkingChatClient.TurnResultKey) == true)
{
var result = update.AdditionalProperties[ThinkingChatClient.TurnResultKey] as TurnResult;
Console.WriteLine($"\nTokens: {result?.Metrics.TotalTokens}");
}
}
Live reasoning separation (opt-in)
Open-source / local providers (DeepSeek, Qwen3, vLLM-served) emit reasoning inline in the text delta as
<think>…</think> rather than as a separate channel. Enable SeparateReasoningInStream to have IndexThinking
split it live — answer text arrives as TextContent, reasoning as TextReasoningContent — even when a tag is
split across chunk boundaries. No bespoke tag parser needed. Default is off (raw pass-through). Native reasoning
providers (OpenAI o-series, Anthropic, Gemini) already emit a separate channel, so this is a no-op for them.
Migration note: with separation enabled, reasoning spans are no longer part of
ChatResponseUpdate.Text(which concatenatesTextContentonly). Consumers readingupdate.Textmust switch to iteratingupdate.Contents(as below) or reasoning deltas will be silently dropped.
var client = new ChatClientBuilder(innerClient)
.UseIndexThinking(o =>
{
o.SeparateReasoningInStream = true; // default false
// o.StreamingReasoningStartTag = "<think>"; // defaults shown
// o.StreamingReasoningEndTag = "</think>";
})
.Build(serviceProvider);
await foreach (var update in client.GetStreamingResponseAsync(messages))
{
foreach (var content in update.Contents)
{
if (content is TextReasoningContent reasoning)
RenderThinking(reasoning.Text); // live "💭 thinking…" UI
else if (content is TextContent answer)
RenderAnswer(answer.Text);
}
}
Supported Providers
| Provider | Reasoning Format | Truncation Handling | Requires Activation |
|---|---|---|---|
| OpenAI | reasoning field |
length, content_filter |
No (automatic) |
| Anthropic | thinking blocks |
max_tokens, refusal |
No (automatic) |
| Google Gemini | thoughtSignature |
MAX_TOKENS, SAFETY |
No (automatic) |
| DeepSeek/Qwen | <think> tags |
OpenAI-compatible | Yes (EnableReasoning) |
| vLLM/GPUStack | Configurable tags | length |
Yes (EnableReasoning) |
Enabling Reasoning for DeepSeek/vLLM/Qwen
Some providers require explicit reasoning activation:
var client = new ChatClientBuilder(innerClient)
.UseIndexThinking(o => o.EnableReasoning = true) // adds the provider's reasoning flag to requests
.Build(serviceProvider);
Inline Reasoning Stripping (Automatic)
When reasoning is disabled on continuation requests (to avoid double-billing thinking tokens), open-source models sometimes output inline reasoning text instead of properly tagged content. IndexThinking automatically strips these fragments:
StripLeadingUntaggedReasoning— removes reasoning paragraphs that appear at the start of a continuation fragment (≥2 consecutive reasoning paragraphs required before stripping)StripUntaggedReasoning— removes trailing reasoning that appears after the actual answer content (requires ≥200 chars of content before the marker and ≥200 chars of trailing reasoning)
Both methods are called automatically by the continuation pipeline. They are also available as static methods on OpenSourceReasoningParser for manual use.
Documentation
License
MIT License - See LICENSE for details.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- IndexThinking (= 0.33.0)
- Microsoft.Data.Sqlite (>= 10.0.12)
- SQLitePCLRaw.bundle_e_sqlite3 (>= 3.0.5)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated | |
|---|---|---|---|
| 0.33.0 | 138 | 10/19/2026 | |
| 0.32.0 | 112 | 10/18/2026 | |
| 0.31.0 | 116 | 10/18/2026 | |
| 0.30.0 | 112 | 10/17/2026 | |
| 0.29.0 | 118 | 10/16/2026 | |
| 0.28.0 | 113 | 10/16/2026 | |
| 0.27.0 | 116 | 10/16/2026 | |
| 0.26.0 | 125 | 10/11/2026 | |
| 0.25.1 | 118 | 10/11/2026 | |
| 0.25.0 | 123 | 10/6/2026 | |
| 0.24.2 | 122 | 10/4/2026 | |
| 0.24.1 | 126 | 10/4/2026 | |
| 0.24.0 | 122 | 10/2/2026 | |
| 0.23.1 | 127 | 10/1/2026 | |
| 0.23.0 | 126 | 9/26/2026 |