Skip to content
tnsaiDocs
GitHub

Context management

tnsai-intelligence has three groups of context tools:

GroupPackageWhat it does
Compactioncom.tnsai.context.compactionShrinks a message list to fit a token budget
Context graphcom.tnsai.context.integrationRecords snapshots and decision traces for an agent
Sessionscom.tnsai.session and subpackagesRecords conversations, searches them, extracts knowledge and consolidates memory

None of them runs on its own. You call the compactors and the session tools from your code. The context graph records chat calls once you turn it on for an agent.

Compaction

A compactor takes a message list (List<Map<String, Object>>, each map with role and content) and a CompactionConfig, and returns a CompactionResult:

import com.tnsai.context.compaction.CompactionConfig;
import com.tnsai.context.compaction.CompactionResult;
import com.tnsai.context.compaction.ContextCompactor;
import com.tnsai.context.compaction.TruncatingCompactor;

ContextCompactor compactor = new TruncatingCompactor();
CompactionConfig config = new CompactionConfig();

if (compactor.shouldCompact(currentTokens, maxTokens, config)) {
    CompactionResult result = compactor.compact(messages, config);
    List<Map<String, Object>> compacted = result.messages();
}

shouldCompact returns true when currentTokens / maxTokens reaches thresholdRatio. compact itself does not check the token count. estimateTokens(messages) gives the compactor's own estimate.

CompactorLLM callsWhat it changes
TruncatingCompactorNoneShortens long tool results and tool-call arguments
LLMContextCompactorOneReplaces older messages with a summary
TwoPhaseCompactorOneRuns the truncating compactor, then the LLM compactor

TruncatingCompactor

new TruncatingCompactor() cuts at 200 characters. new TruncatingCompactor(maxArgLength) sets another limit, at least 1.

It never removes a message. Outside the system prompt and the last preserveLastN messages, it cuts:

  • the content of messages with role tool;
  • tool-call arguments, either function.arguments or a top-level arguments.

A cut value keeps its first maxArgLength characters and ends with ...[truncated]. When nothing is long enough to cut, the result reports no compaction.

LLMContextCompactor

import com.tnsai.context.compaction.LLMContextCompactor;

LLMContextCompactor compactor = new LLMContextCompactor(summarizerClient);
CompactionResult result = compactor.compact(messages, config);

The compactor keeps the system prompt and the last preserveLastN messages. It sends everything in between to the client in one call, built from summaryPromptTemplate with {messages} replaced by [role]: content lines. The result is:

  1. the system prompt, if kept;
  2. one system message, [Conversation Summary] followed by the summary;
  3. the preserved messages.

If the call fails, the summary text says so: [Previous N messages summarized - summarization failed: …].

With toolResultPruningEnabled(true), long tool results are shortened before summarizing. A message counts as a tool result when its role is tool or its content starts with Tool execution results:. A result over toolResultPruningThreshold tokens becomes [<tool> result: N chars, <first 100 characters>...].

TwoPhaseCompactor

import com.tnsai.context.compaction.TwoPhaseCompactor;

TwoPhaseCompactor compactor = new TwoPhaseCompactor(
    new TruncatingCompactor(),
    new LLMContextCompactor(summarizerClient));

Both arguments are required. Phase 2 always runs after phase 1. The compactor has no token budget, so it cannot tell whether truncation alone was enough. Use shouldCompact before calling it.

CompactionConfig

Build it with CompactionConfig.builder(), new CompactionConfig() (all defaults) or new CompactionConfig(thresholdRatio, preserveLastN).

FieldDefaultMeaning
thresholdRatio0.80Ratio for shouldCompact. Above 0, at most 1.
preserveLastN5Recent messages left unchanged
preserveSystemPrompttrueKeep the first system message
minMessagesForCompaction10Below this, nothing is compacted
summaryPromptTemplatebuilt-inSummary prompt, with a {messages} placeholder
toolResultPruningEnabledfalseShorten tool results before summarizing
toolResultPruningThreshold500Token size above which a tool result is shortened
summarizerModelnullStored only. The compactor uses the client you pass.
maxSummaryTokens500Stored only. The summary length is not limited.

CompactionResult

AccessorMeaning
messages()The new message list
compactionPerformed()False when nothing changed
originalMessageCount(), compactedMessageCount(), summarizedMessageCount()Message counts
originalTokens(), compactedTokens()Token estimates before and after
summary()The summary text, or null
tokenReductionRatio(), messageReduction()Derived figures

Context graph

AgentContextManager records snapshots of an agent's context and traces of its decisions.

Turn it on for an agent

import com.tnsai.context.decision.impl.InMemoryDecisionTraceStore;
import com.tnsai.context.snapshot.impl.InMemorySnapshotStore;

agent.enableContextGraph(new InMemorySnapshotStore(), new InMemoryDecisionTraceStore());

agent.getContextManager().ifPresent(cm -> cm.getRecentDecisions(10));

enableContextGraph finds the manager through a service file in tnsai-intelligence. Without that module on the classpath, it logs a warning and does nothing, and getContextManager() stays empty.

Once it is on, each chat call:

  • starts a conversation if none is open;
  • records a decision trace with action CHAT, the message as trigger and the reply or error as outcome.

agent.stop() ends the conversation. Tool calls and actions are not traced.

Use it directly

import com.tnsai.context.integration.AgentContextManager;

AgentContextManager contextManager = new AgentContextManager(
    "agent-001",
    new InMemorySnapshotStore(),
    new InMemoryDecisionTraceStore());

Snapshots

A snapshot holds the tracked entities, the context variables and the current conversation id. diffSnapshots(beforeId, afterId) compares two of them.

Agent has no getBeliefs() / getDesires() / getIntentions(). Use the trigger-only snapshot, or pass application-owned BDI lists if you maintain them yourself.

package com.example.tnsai.docs;

import com.tnsai.context.integration.AgentContextManager;
import com.tnsai.context.snapshot.ContextSnapshot;
import com.tnsai.context.snapshot.SnapshotDiff;
import com.tnsai.context.snapshot.SnapshotTrigger;

import java.util.List;
import java.util.Optional;

public final class ContextSnapshotExample {
    private ContextSnapshotExample() {}

    public static ContextSnapshot capture(AgentContextManager contextManager) {
        return contextManager.createSnapshot(SnapshotTrigger.PRE_DECISION);
    }

    public static Optional<SnapshotDiff> recentDiff(
        AgentContextManager contextManager,
        String beforeId,
        String afterId
    ) {
        List<ContextSnapshot> recent = contextManager.getRecentSnapshots(10);
        return recent.isEmpty()
            ? Optional.empty()
            : contextManager.diffSnapshots(beforeId, afterId);
    }
}

createSnapshot takes a SnapshotTrigger such as PRE_DECISION, POST_DECISION, MANUAL or CHECKPOINT. findSnapshot(id) and getRecentSnapshots(limit) read them back.

Decision traces

executeWithTrace runs an operation and records it:

String reply = contextManager.executeWithTrace(
    "Summarize report",
    () -> agent.chat("Summarize this..."));

It takes a snapshot before and after, runs the operation and saves a trace with the outcome. If the operation throws, the trace records the failure and the exception is thrown again.

A five-argument overload also takes suppliers of Belief, Desire and Intention lists. The first intention's task description becomes the trace action, in upper case with underscores. The two-argument form records no action, so findDecisionsByAction does not find its traces.

createTrace(DecisionTrace.Builder) saves a trace you build yourself.

Query traces

MethodReturns
getRecentDecisions(limit), getAllDecisions()This agent's traces
findDecisionsInCurrentConversation()Traces in the open conversation, empty when none is open
findDecisionsByAction(action)Traces with that action
findSuccessfulDecisions(), findFailedDecisions()Traces by outcome
findTrace(id), explainDecision(id)One trace, or its explanation text
findPrecedents(trace, threshold)Traces similar to the given one
summarizeRecentDecisions(limit)One summary line per recent trace

The action, outcome and precedent queries search the whole store, not only this agent's traces. With the in-memory store, a precedent is scored on two things: the same action and the same trigger type.

Entities, variables and conversations

MethodMeaning
trackEntity(entity), untrackEntity(id), getTrackedEntities(), clearTrackedEntities()Entities included in snapshots
setVariable(key, value), getVariable(key), getVariable(key, type), removeVariable(key), clearVariables()Variables included in snapshots
startConversation(), startConversation(id)Open a conversation and take a CONVERSATION_START snapshot
ensureConversation()Return the open conversation id, or open one
endConversation()Take a CONVERSATION_END snapshot and close the conversation
getCurrentConversationId()The open conversation, if any

Statistics and cleanup

getDecisionCount(), getSnapshotCount(), getTraceStatistics() and getSnapshotStatistics() report store sizes. cleanupOldTraces(instant) and cleanupOldSnapshots(instant) delete entries older than the cutoff and return how many were deleted.

Sessions

AgentSession records one conversation. You add the turns yourself; agents do not write sessions.

import com.tnsai.session.AgentSession;

AgentSession session = AgentSession.start("code-assistant", "/path/to/project");

session.addUserMessage("Fix the authentication bug")
    .addAssistantMessage("I'll look at the auth module.")
    .addToolCall("readFile", Map.of("path", "Auth.java"))
    .addToolResult("readFile", "public class Auth { ... }")
    .addMetadata("model", "gpt-4o")
    .addTags("security", "bug-fix");

session.success();

fail(error) stores the error under the metadata key error. cancel() and complete(outcome) end the session with another outcome. The outcome starts as IN_PROGRESS; the others are SUCCESS, FAILED, CANCELLED, TIMEOUT and MAX_ITERATIONS.

MethodReturns
getTurns(), getTurnCount(), getLastTurn()Turns
getTurnsByType(TurnType)Turns of one type: USER_MESSAGE, ASSISTANT_MESSAGE, SYSTEM_MESSAGE, TOOL_CALL, TOOL_RESULT, ERROR, CHECKPOINT
getFirstUserMessage()The first user message, or null
getDuration()Time from start to end, or to now while in progress
isComplete(), isSuccess()Outcome checks

Store sessions

import com.tnsai.session.SessionStore;

SessionStore store = SessionStore.file(".tnsai/history/");

store.save(session);
Optional<AgentSession> loaded = store.load(session.getId());
List<AgentSession> recent = store.findRecent(10);

FileSessionStore writes one JSON file per session in a folder per date, plus an index.json. new FileSessionStore(path, maxAgeDays) deletes sessions older than maxAgeDays when it is created and when you call evictExpired().

Other queries: findByAgent, findByProject, findByTag, findByOutcome, findByDate, findByDateRange, listSessionIds, count, exists, delete and clear.

Search sessions

SearchableSessionStore.file(path) adds text search over turn contents:

import com.tnsai.session.SearchableSessionStore;
import com.tnsai.session.SessionQuery;

SearchableSessionStore store = SearchableSessionStore.file(".tnsai/history/");

SessionQuery query = SessionQuery.builder()
    .text("authentication bug")
    .agent("code-assistant")
    .tags("security")
    .outcome(SessionOutcome.SUCCESS)
    .lastDays(7)
    .limit(20)
    .build();

List<SessionSearchResult> results = store.search(query);

All filters must match, and a session must have every tag listed. limit defaults to 50. Text matching ignores case unless caseSensitive(true) is set.

SearchModeMatches
ANY (default)Any of the words
ALLAll of the words, in the order given
EXACTThe exact phrase
REGEXThe text as a regular expression, at most 500 characters

A regular expression from untrusted input can take a long time to match. Prefer the other modes for such input.

SessionQuery.text(text) and SessionQuery.recent(limit) are shortcuts. findSimilar(sessionId, limit) and findByToolUsage(toolName) are also available.

Extract knowledge

LLMKnowledgeExtractor asks an LLM for reusable knowledge in a session:

import com.tnsai.session.extraction.ExtractionType;
import com.tnsai.session.extraction.KnowledgeExtractor;
import com.tnsai.session.extraction.LLMKnowledgeExtractor;

KnowledgeExtractor extractor = new LLMKnowledgeExtractor(llmClient, 0.5);

List<ExtractedKnowledge> all = extractor.extract(session);
List<ExtractedKnowledge> decisions = extractor.extract(session,
    EnumSet.of(ExtractionType.DECISION, ExtractionType.LEARNING));

It makes one LLM call per type, so five calls for all types. Turns longer than 2,000 characters are cut. Items below the minimum confidence are dropped; the default is 0.3. An answer that cannot be parsed gives no items for that type, with no error. withMinConfidence(value) returns a copy with another minimum.

ExtractionTypeKeyMeaning
SOLUTIONproblem-solutionA problem and how it was solved
PATTERNpatternA reusable code or design pattern
DECISIONdecisionA design decision with its rationale
LEARNINGlearningA lesson learned
ANTIPATTERNantipatternAn approach to avoid

An ExtractedKnowledge record has content, summary, problem, codeSnippets, relatedFiles, tags, confidence and extractedAt. isHighConfidence() means 0.8 or more.

Store knowledge

import com.tnsai.session.extraction.KnowledgeStore;

KnowledgeStore store = KnowledgeStore.file(".tnsai/knowledge/");   // or KnowledgeStore.inMemory()

store.saveAll(all);
List<ExtractedKnowledge> solutions = store.listByType(ExtractionType.SOLUTION);
List<ExtractedKnowledge> found = store.search("connection pool");

The file store writes one JSON file per item in a folder per type. Other methods: load, delete, listBySession, listRecent, findSimilar, count, countByType, getStats and clear.

Consolidate memory

MemoryConsolidationPipeline extracts knowledge from a session, stores it and summarizes the conversation:

import com.tnsai.session.consolidation.ConsolidationResult;
import com.tnsai.session.consolidation.ForgettingCurve;
import com.tnsai.session.consolidation.MemoryConsolidationPipeline;

MemoryConsolidationPipeline pipeline = MemoryConsolidationPipeline.builder()
    .knowledgeExtractor(extractor)
    .knowledgeStore(store)
    .contextCompactor(new LLMContextCompactor(summarizerClient))
    .compactionConfig(new CompactionConfig())
    .forgettingCurve(ForgettingCurve.EXPONENTIAL)
    .minKnowledgeConfidence(0.5)
    .build();

ConsolidationResult result = pipeline.consolidate(session);

Only knowledgeExtractor is required. consolidate runs these steps:

  1. Extract knowledge and keep items at or above minKnowledgeConfidence (default 0.5).
  2. With a store and a curve other than NONE, delete stored items whose decayed confidence is below 0.1. Only the 100 most recent items are checked.
  3. Save the kept items to the store, if there is one.
  4. With a compactor and a config, compact the session turns and keep the summary.

The session itself is not changed. A failure in one step is logged and the other steps still run.

ConsolidationResult has sessionId, summary, extractedKnowledge, knowledgeCount(), hasKnowledge(), originalMessageCount, consolidatedMessageCount and duration.

ForgettingCurveDecayed confidence after d days
NONE (default)Unchanged
LINEARconfidence × max(0, 1 − 0.01 d)
EXPONENTIALconfidence × e^(−0.05 d)

Consolidate automatically

AutoConsolidationManager runs the pipeline when a trigger fires:

import com.tnsai.session.consolidation.AutoConsolidationManager;
import com.tnsai.session.consolidation.ConsolidationTrigger;

AutoConsolidationManager manager = AutoConsolidationManager.builder()
    .pipeline(pipeline)
    .triggers(Set.of(ConsolidationTrigger.MESSAGE_COUNT, ConsolidationTrigger.ON_SESSION_END))
    .messageCountThreshold(50)
    .build();

ConsolidationResult result = manager.onMessageAdded(session, estimatedTokens);  // after each message
manager.onSessionEnd(session);                                                // when the session ends
TriggerFires whenBuilder setting
MESSAGE_COUNTonMessageAdded has been called this many times since the last consolidationmessageCountThreshold, default 50
TOKEN_LIMITestimatedTokens is above the given percentage of the budgettokenBudget, default 8192; tokenThresholdPercent, default 80
ON_SESSION_ENDonSessionEnd is called
TIME_INTERVALNever. The manager does not handle it.

The default triggers are MESSAGE_COUNT, TOKEN_LIMIT and ON_SESSION_END. Both methods return null when nothing ran. A trigger that fires while a consolidation is running is skipped. A failed consolidation is logged, returns null and does not reset the message count.