Bound context and understand compaction
Goal: avoid sending a request beyond the configured context ceiling while keeping prompt size, retained history and permissions distinct.
1. Start from a working agent
Use the native calculator tutorial. Its Touba program remains unchanged. Use this provider:
import { createOpenAiDecisionProvider } from "@touba/sdk";
export const decideAgentCapability = createOpenAiDecisionProvider({
model: "gpt-4.1-mini",
decisionInterface: "native",
maxOutputTokens: 1000,
maxRetries: 0,
context: {
contextWindowTokens: 32768,
safetyMarginTokens: 512,
inputTokenMultiplier: 1.2,
onEvent(event) { console.error(JSON.stringify(event)); },
},
history: {
maxMessages: 128,
maxBytes: 262144,
maxRetainedBytes: 8 * 1024 * 1024,
onEvent(event) { console.error(JSON.stringify(event)); },
},
});
32768 is an application ceiling here, not the model's advertised maximum.
Verify that your model supports at least this window. Changing models requires
rechecking the window, output reservation and provider parameters.
context explicitly enables counting; the language embeds no aging model catalogue.
2. Read the events
Run the calculator using the native tutorial's command. Official counting precedes each generation and receives the serialized body, including system instructions, tools, schemas and history. Events go to stderr, keeping the CLI's JSON result on stdout.
For example, with 1000 counted input tokens these options reserve 1200 + 512 + 1000 = 2712 tokens. The last term reserves the response, including reasoning accounted as output by the provider. Counts may be estimates; the margin covers reasonable variation without guaranteeing future external API behavior.
| Event / error | Meaning |
|---|---|
provider_context_count, fits: true | the ceiling permits sending |
fits: false | no generation; attempt local context reduction |
decision_history_context | message size and exchanges omitted from the window |
ProviderContextWindowExceeded | even the retained minimum does not fit |
ProviderContextCountUnavailable | counting unavailable; generation was not sent |
3. Trigger a controlled refusal
In a copy of the provider, set contextWindowTokens: 100, keeping
maxOutputTokens: 1000. Run the calculator again: the output reservation alone
exceeds the ceiling. Expect refusal before generation and before selecting any
skill. If counting is unavailable, that error takes precedence; generation
still is not sent.
Restore the original ceiling afterwards. Do not classify this refusal as a
successful business result. The runtime error's provider_error_code exposes
the SDK diagnosis when available.
4. Understand what is reduced
The SDK removes the oldest complete exchanges from sent context. A native call stays paired with its result and original metadata. The latest exchange and all current runtime observations remain. Instructions, capability signatures and permissions are not summarized.
Originals remain in the store until expiration, explicit deletion or a storage capacity refusal. If current observations alone are too large, no summary can bypass that limit: reconsider result size, budgets or work decomposition.
5. Add a summary only when needed
Default compaction calls no summarizer. If your application has a qualified
summary service, provide history.summarize. Its contract is documented in the
history guide. It receives outgoing
exchanges and the previous summary, and returns bounded text. The SDK adds it
as untrusted data with provenance and counts the request again. Original
exchanges remain available in storage.
A summary may omit details or contain malicious instructions. It is never evidence of authorization or human approval. If the service uses an LLM, the host must account for its costs, quotas and timeouts; these extra calls are not runtime decisions.
6. Use Anthropic or Gemini
The guard is independent of Touba syntax. With the AI SDK connectors installed in your application, configure their transport:
import { createAnthropic } from "@ai-sdk/anthropic";
import { createAiSdkDecisionProvider, createProviderContextFetch } from "@touba/sdk";
const transport = createProviderContextFetch({
provider: "anthropic",
contextWindowTokens: 32768,
});
const anthropic = createAnthropic({ fetch: transport });
export const decideAgentCapability = createAiSdkDecisionProvider({
model: anthropic(process.env.ANTHROPIC_MODEL),
decisionInterface: "native",
maxOutputTokens: 1000,
maxRetries: 0,
});
For Gemini, use createGoogleGenerativeAI from @ai-sdk/google with
provider: "google", an explicit model and GOOGLE_GENERATIVE_AI_API_KEY.
Your application must install these connectors and configure their keys.
These examples target official direct endpoints, not Bedrock or Vertex.
The guard refuses unsupported endpoints without sending the request.
A token ceiling does not replace a monetary budget, Touba budgets or business validation. Also see the SQLite tutorial.