# Context management

`tnsai-intelligence` has three groups of context tools:

| Group | Package | What it does |
|---|---|---|
| Compaction | `com.tnsai.context.compaction` | Shrinks a message list to fit a token budget |
| Context graph | `com.tnsai.context.integration` | Records snapshots and decision traces for an agent |
| Sessions | `com.tnsai.session` and subpackages | Records 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`:

```java
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.

| Compactor | LLM calls | What it changes |
|---|---|---|
| `TruncatingCompactor` | None | Shortens long tool results and tool-call arguments |
| `LLMContextCompactor` | One | Replaces older messages with a summary |
| `TwoPhaseCompactor` | One | Runs 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`

```java
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`

```java
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)`.

| Field | Default | Meaning |
|---|---|---|
| `thresholdRatio` | `0.80` | Ratio for `shouldCompact`. Above 0, at most 1. |
| `preserveLastN` | `5` | Recent messages left unchanged |
| `preserveSystemPrompt` | `true` | Keep the first system message |
| `minMessagesForCompaction` | `10` | Below this, nothing is compacted |
| `summaryPromptTemplate` | built-in | Summary prompt, with a `{messages}` placeholder |
| `toolResultPruningEnabled` | `false` | Shorten tool results before summarizing |
| `toolResultPruningThreshold` | `500` | Token size above which a tool result is shortened |
| `summarizerModel` | `null` | Stored only. The compactor uses the client you pass. |
| `maxSummaryTokens` | `500` | Stored only. The summary length is not limited. |

### `CompactionResult`

| Accessor | Meaning |
|---|---|
| `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

```java
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

```java
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.

<!-- java-contract: src/main/java/com/example/tnsai/docs/ContextSnapshotExample.java -->
```java
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:

```java
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

| Method | Returns |
|---|---|
| `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

| Method | Meaning |
|---|---|
| `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.

```java
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`.

| Method | Returns |
|---|---|
| `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

```java
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:

```java
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.

| `SearchMode` | Matches |
|---|---|
| `ANY` (default) | Any of the words |
| `ALL` | All of the words, in the order given |
| `EXACT` | The exact phrase |
| `REGEX` | The 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:

```java
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.

| `ExtractionType` | Key | Meaning |
|---|---|---|
| `SOLUTION` | `problem-solution` | A problem and how it was solved |
| `PATTERN` | `pattern` | A reusable code or design pattern |
| `DECISION` | `decision` | A design decision with its rationale |
| `LEARNING` | `learning` | A lesson learned |
| `ANTIPATTERN` | `antipattern` | An approach to avoid |

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

### Store knowledge

```java
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:

```java
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`.

| `ForgettingCurve` | Decayed confidence after `d` days |
|---|---|
| `NONE` (default) | Unchanged |
| `LINEAR` | `confidence × max(0, 1 − 0.01 d)` |
| `EXPONENTIAL` | `confidence × e^(−0.05 d)` |

### Consolidate automatically

`AutoConsolidationManager` runs the pipeline when a trigger fires:

```java
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
```

| Trigger | Fires when | Builder setting |
|---|---|---|
| `MESSAGE_COUNT` | `onMessageAdded` has been called this many times since the last consolidation | `messageCountThreshold`, default 50 |
| `TOKEN_LIMIT` | `estimatedTokens` is above the given percentage of the budget | `tokenBudget`, default 8192; `tokenThresholdPercent`, default 80 |
| `ON_SESSION_END` | `onSessionEnd` is called | |
| `TIME_INTERVAL` | Never. 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.

## Related

- [Reasoning](reasoning.md): executors for multi-step reasoning.
- [Learning](learning.md): improving from feedback.
