The two scopes of memory
A ContextWindow is the local reservoir. A ContextBank is the global singleton that holds those windows across pipes, pipelines, and processes. Two scopes of the same memory system: the window is the agent’s working memory, the bank is the project’s long-term memory.
Here’s the whole API surface in one block:
import com.TTT.Context.ContextBank
import com.TTT.Context.ContextWindow
// Local working memory for one pipe execution
val window = ContextWindow().apply {
addLoreBookEntry(
key = "merchant_guild",
value = "A powerful trade consortium in the capital city. Controls river trade up to the northern provinces.",
weight = 5,
aliasKeys = listOf("guild", "merchants", "trade")
)
contextElements.add("The current date is the 14th of Hethmoon, year 412.")
}
// Persist it across the process
ContextBank.emplaceWithMutex("campaign_facts", window)
That’s the foundation. The window is the data structure. The bank is the persistence layer. The mutex is the concurrency contract.
What’s actually in a ContextWindow
The data class is defined in Context/ContextWindow.kt and serializes cleanly. Three slots:
@Serializable
data class ContextWindow(@Transient val isInitialized: Boolean = false)
{
var loreBookKeys = mutableMapOf<String, LoreBook>()
var contextElements = mutableListOf<String>()
var converseHistory = ConverseHistory()
var version: Long = 0
@Transient var metaData = mutableMapOf<Any, Any>()
}
loreBookKeys— weighted, key-triggered entries. EachLoreBookcarries a primary key, alias keys, a weight, optional linked keys (cascade-activate on hit), and optional required keys (dependency gating). Selection runs at pipe execution time: scan the prompt for matches, fit the budget.contextElements— raw strings. Anything that should always be available to the LLM: instructions, rules, scratch state, the current date.converseHistory— structured conversation turns. Role-tagged messages (user, assistant, system) with multimodal content. Different from a flat list of strings because the LLM can be told which turn was which role.
version is for remote synchronization — when the bank is backed by a remote store, the version field tracks write conflicts. metaData is a transient escape hatch for system-internal bookkeeping; the docs are explicit that users should leave it alone.
What’s actually in ContextBank
The bank is a Kotlin object — a process-wide singleton. The map is a ConcurrentHashMap<String, ContextWindow>. There are four mutexes:
object ContextBank
{
private val bank = ConcurrentHashMap<String, ContextWindow>()
val swapMutex = Mutex() // guards the active banked window
val bankMutex = Mutex() // global bank-wide write coordination
val todoMutex = Mutex() // for the todo list system
private val pageMutexes = ConcurrentHashMap<String, Mutex>() // per-key locks
// ...
}
Three of those mutexes are public. The fourth, pageMutexes, is per-key — each named window gets its own lock, so unrelated keys can be read and written concurrently without contention. The emplaceWithMutex family uses the per-key mutex; swapBankWithMutex uses the global swap mutex; bankMutex is for operations that span multiple keys.
Read-modify-write is two operations. Two coroutines that read the same window, each mutate their local copy, and each write back — one update gets silently lost. The mutex collapses the read, the mutate, and the write into a single critical section.
The Autogenesis pattern
Two agents, one bank, one key, two scopes of the same data:
// writerAgent.kt:793-799 — write a generated chapter back to the bank
val storyData = ContextBank.getContextFromBank("story")
storyData.contextElements.add(it.text)
ContextBank.emplaceWithMutex("story", storyData)
The WriterAgent runs the LLM generation, then appends the generated text to the story window’s contextElements. The window is held under the key "story" in the bank. The mutex makes the read-modify-write atomic.
// lorebookAgent.kt:166-280 — extract structured entities, update lorebook entries
val storyContext = ContextBank.getContextFromBank("story")
for (char in extraction.characters) {
val existing = storyContext.loreBookKeys[char.name]
val merged = if (existing != null) existing.combineValue(char.asLoreBook()) else char.asLoreBook()
storyContext.addLoreBookEntry(
key = char.name,
value = serialize(merged),
weight = char.weight,
aliasKeys = char.aliases
)
}
// ... same loop for events, locations, items, factions, relationships
ContextBank.emplaceWithMutex("story", storyContext)
The LorebookAgent reads the "story" window the WriterAgent just wrote to. It runs an LLM extraction step over the recent contextElements and folds the structured entities into the window’s loreBookKeys slot. Then it writes the updated window back to the bank under the same key.
The WriterAgent writes raw text to the bank. The LorebookAgent reads it back and folds structured entries into the same window’s lorebook slot. A third agent, the chapter generator, pulls structured memory and writes the next chapter.
The whole pattern is read from the bank, do work, write back. The bank is the seam between agents. The mutex is the contract that makes the seam safe.
Pulling context into a pipe
The bank is also the source for pipe-level context injection. The pipe-side helpers handle the plumbing:
val storyPipe = BedrockPipe()
.pullGlobalContext()
.setPageKey("story")
.autoInjectContext("The user prompt contains story context. Use it to ground your response.")
pullGlobalContext enables bank reads at the pipe level. setPageKey("story") scopes the read to one named window. autoInjectContext is the instruction that tells the LLM what to do with the injected data.
At execution time, the framework reads the window from the bank, runs lorebook selection against the user prompt, fits the result to the token budget, and injects the selected context into the prompt. The pipe’s body never touches the bank directly. The bank is the storage layer; the pipe helpers are the retrieval layer.
Multiple keys can be passed as a comma-separated string. setPageKey("story, session_$userId, world_rules") reads three windows, merges them, and runs selection against the merged set. The merge strategy is configurable; the default is to emplace lorebook entries and append context elements.
When to use the bank vs a local window
The decision rule is whether the data needs to survive the current execution.
- Use a local ContextWindow for transient state: scratch data, intermediate results, in-flight computations. Anything that dies when the pipe returns.
- Use ContextBank for state that a future agent in a future execution will need: persistent character bios, accumulated world facts, cross-pipeline handoffs.
The isInitialized transient flag on the window exists for this. A local window can be created and discarded freely. A bank-held window has a key and a lifecycle that outlives the pipe that wrote it.
Most production code uses both: local windows for working state, the bank for anything persistent. The WriterAgent holds its generation in flight locally and writes the final result to the bank. The LorebookAgent reads, mutates, and writes back. The bank is the spine.
The next post
The next post in this series covers the full memory agent pattern: a pipeline that runs after every generation, extracts structured entities, folds them into the lorebook, and persists the result in real time with no human in the loop.
Same data structures. Same bank. The difference is a pipeline that turns generated text into structured, retrievable memory on every turn.
Most production agents have no memory worth mentioning. They re-derive context from a vector store on every call, return the same three chunks for every query, and forget what happened in the last session the moment it ends. The memory agent pattern is the fix: a pipeline that writes its own lorebook every turn, automatically, so the next turn starts where the last one left off. That’s the next post.
Related posts
- What TPipe’s Memory System Actually Is — The architectural pitch for LoreBook vs RAG. This post is the implementation; that post is the reasoning.
- Headless AI Agents: What, Why, and How — The headless agent pattern. Memory agents are a headless agent that writes its own state.
- Building Your First TPipe Pipeline — The pipeline build that uses these data structures end-to-end.
- The KillSwitch: Token Budgets That Actually Kill the Agent — The cost-control post. The bank is the persistence layer; the KillSwitch is the enforcement layer.