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. Each LoreBook carries 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.

  • Persistent Memory for AI Agents — TPipe’s ContextBank is the substrate-level persistent memory layer. Thread-safe, lorebook-weighted, global across sessions and distributed systems.

Frequently Asked Questions

What is the difference between ContextWindow and ContextBank in TPipe?
A ContextWindow is the local, serializable data class that holds the three memory slots — lorebook entries, context elements, and conversation history — for a single pipe execution. A ContextBank is the Kotlin singleton (`object ContextBank`) that holds a `ConcurrentHashMap` accessible across pipes, pipelines, and processes. The window is the agent's working memory for one execution. The bank is the project's long-term memory across all executions. The two scopes share a single API surface: everything you do to a window locally, you can persist globally by emplacing the window in the bank under a named key.
When should I write to ContextBank vs keep context local to a pipe?
Write to the bank when the data needs to survive the current pipe execution and be available to other pipes, other pipelines, or future requests. Keep it local when the data is short-lived and only relevant to the current execution. The pattern in Autogenesis is to keep transient scratch state in a local MiniBank and write anything that the next agent needs into ContextBank with a stable key. The LorebookAgent reads from the bank, extracts structured entries, and writes them back under the same key. The WriterAgent appends generated text to the bank's `contextElements`. Both depend on the bank's persistence across executions.
What is the lorebook slot in ContextWindow for?
The `loreBookKeys` slot stores weighted, key-triggered context entries. Each entry has a primary key, optional alias keys, a weight, optional linked keys (cascade-activate on hit), and optional required keys (dependency gating). When a pipe's lorebook selection runs, it scans the user prompt for matching keys, applies weighting, expands linked entries, checks dependencies, and selects the entries that fit the token budget. The lorebook slot is the keyword-triggered recall layer. Entries fire on explicit substring matches against the user prompt.
Why does ContextBank use mutex operations?
ContextBank is a singleton, and multiple coroutines in a single process can call it concurrently. Without mutex protection, two writers reading the same window, mutating it locally, and writing it back will lose one of the updates — the classic read-modify-write race. The `emplaceWithMutex` family acquires the bank's mutex for the duration of the operation, performs the write atomically, and releases. The non-mutex variants are exposed for code paths that already hold a lock or are demonstrably single-threaded. In pipes and pipelines, always use the `*WithMutex` variants. ContextBank.kt:74 declares `val bankMutex = Mutex()` and that mutex is what protects the concurrent write hazard.
Can multiple pipelines share context through ContextBank?
Yes. The bank is process-wide, not pipeline-scoped. Two pipelines that reference the same key read and write the same ContextWindow. The Autogenesis orchestrator uses this: the WriterAgent pipeline writes generated prose into the `story` page, the LorebookAgent pipeline reads the same page, extracts structured entities, and updates the lorebook entries, and a downstream generation pipeline reads the same `story` page to ground the next chapter. The bank is the seam between pipelines. It is also the seam between requests: a long-running agent can persist state across restarts by reading and writing through the bank's storage-backed keys.
What is the difference between emplaceWithMutex and a plain write?
A plain `emplace` writes the new window directly into the bank's `ConcurrentHashMap`. `ConcurrentHashMap` makes a single put atomic. The read-modify-write cycle is two operations, and two writers can both read the old value, both write a new value that includes their own update, and one update is silently lost. `emplaceWithMutex` acquires `bankMutex` before reading and writing, so the entire read-modify-write cycle runs as a critical section. The mutex is per-key for `emplaceWithMutex` (each key gets its own lock in `pageMutexes`) and global for `swapBankWithMutex` (which guards the active banked window). Use `emplaceWithMutex` for every write from a pipe.
How do pipes pull context from the bank into their own context?
Call `pipe.pullGlobalContext()` to enable bank reads, then `pipe.setPageKey("story")` to scope the read to a single named window. Call `pipe.autoInjectContext("The user prompt contains story context.")` to have the framework inject the selected context into the user prompt at execution time. The `pullGlobalContext` setter is the gate; `setPageKey` is the namespace; `autoInjectContext` is the instruction that tells the LLM what to do with the injected data. Multiple keys can be passed to `setPageKey` as a comma-separated string — the framework merges the windows before selection.