Conversation history and context
An agent needs both a record of its work and a useful set of messages for the next model call. Bub stores the record in a Tape and builds model context from selected entries. Keeping the two separate lets you retain history while changing what the model reads.
This model is shared by core.Agent and BubAgent. Their storage defaults and session naming differ.
Choose where history lives
Section titled “Choose where history lives”Plain Agent creates an InMemoryTapeStore by default. That history lasts for the lifetime of the store. Supply a file store to keep it between processes:
from pathlib import Path
from bub.core import Agent
from bub.core.store import FileTapeStore
agent = Agent(store=FileTapeStore(Path("sessions")))
Each agent.run(..., tape="customer-42") uses the named tape in that store. Reuse the name to continue a conversation; choose another to keep conversations separate. Agent uses the supplied name directly; the application has its own workspace/session naming scheme.
agent.tape(name) gives you a view of that tape outside a run. Its writes go straight to the store:
async def inspect_history() -> None:
tape = agent.tape("customer-42")
messages = await tape.read_messages()
entries = list(await tape.query().all())
print(len(messages), "model messages;", len(entries), "stored entries")
A custom store implements TapeStore or AsyncTapeStore from bub.core.store. Supply it directly as Agent(store=my_store).
| Method | Responsibility |
|---|---|
list_tapes() |
Return stored tape names. |
fetch_all(query) |
Return entries matching the TapeQuery, including its filters and ordering. |
append(tape, entry) |
Append an entry to the named tape. |
reset(tape) |
Clear the named tape. |
Use synchronous methods for TapeStore, asynchronous methods for AsyncTapeStore. The agent adapts a synchronous store for async use. Persistence and coordination across processes belong to the store and your application.
Choose what the model reads
Section titled “Choose what the model reads”Tape entries include messages, tool calls, tool results, events, and anchors. Normal recording appends entries. Use explicit operations such as Tape.reset() to clear history.
The default TapeContext selects conversation entries after the latest anchor. It converts them to Republic messages:
| Entry | Model context |
|---|---|
anchor |
Excluded by the default query. If a custom query includes anchors, the default selector converts them to assistant messages. |
message |
The saved message. |
tool_call |
An assistant message carrying the calls. |
tool_result |
Tool messages paired with the preceding calls. |
| Other events | Omitted by the default selector. |
An anchor marks a new starting point for context. handoff() writes one without deleting earlier entries:
async def start_next_phase() -> None:
await agent.tape("customer-42").handoff(
name="order-reviewed",
state={"summary": "Order A123 has shipped; the customer needs tracking details."},
)
The state here is saved on the anchor, before the default query’s starting point. To carry a summary into the next phase, include it in the next prompt or explicitly select it in your context strategy. Restoring values into the run’s mutable state dictionary also requires your own loading logic. New sessions get a session/start anchor automatically.
Pass context=TapeContext(...) to Agent to select another anchor or use your own select(entries, context) function. A selector returns Republic messages or an awaitable of them. default_tape_context() includes tool exchanges; a bare TapeContext without select uses its simpler message-only conversion. Entries marked context=False are removed before either a default or custom selector runs.
On a recognized context-length error, the agent makes one automatic handoff and retries the prompt with the shorter context. If you need a summary of the earlier history, create it before handing off. The retry can still fail if the remaining request is too large.
Decide what survives a run
Section titled “Decide what survives a run”The state passed to agent.run() is a mutable dictionary shared by tools, prompt sections, and event handlers during that run. Your application owns its persistence: keep durable data in your own store, or write selected facts to the tape and explicitly read them back when needed.
The default TapeRecorder saves conversation exchanges and selected lifecycle records. Record any additional variables, custom events, or decisions explicitly.
Forks and partial results
Section titled “Forks and partial results”Each agent.run() opens a fork of its tape. When the stream closes, the fork appends its new entries to the parent. This also happens after an exception or early cancellation. A failed run can therefore leave useful partial history.
Pass merge_back=False to discard the fork’s records on exit. This affects tape writes only. Tool side effects, such as sent requests or modified files, remain.
For a block of work you control directly, use agent.session(name, merge_back=...) or tape.fork_tape(merge_back=...). Both merge on exit by default, including exceptional exits.
Serialize runs that share a tape when they must see each other’s completed work. Your application provides the session lock. The FastAPI example shows locking within one process; multiple workers need coordination across processes.
Keep large results alongside the conversation
Section titled “Keep large results alongside the conversation”A TapeSidecar stores related data in a separate tape. Mounted sidecars follow the main tape through forks, merges, and resets, while their entries stay out of the main context. Pass sidecars explicitly to Agent(sidecars=...) when your application needs them.
The Bub application mounts a spill sidecar for large tool results. It keeps a bounded preview and a handle in the conversation, then lets the agent read portions with spill.read. During a fork merge or tape reset, sidecar failures are recorded separately while the main tape operation can still complete. The app’s spill limits belong to its configuration.
Storage in the Bub application
Section titled “Storage in the Bub application”BubAgent uses the store provided by BubFramework. The built-in application uses FuzzyFileTapeStore under Bub’s home directory, normally ~/.bub/tapes/. Its tape name combines hashes of the workspace path and session ID so the same session ID in different workspaces has separate history.
Applications replace that store through provide_tape_store and select context through build_tape_context. The SQLAlchemy guide demonstrates this plugin route. For a direct SDK service, pass your implementation to Agent(store=...) as described above.
The built-in app restores selected session settings from tape records. Other turn-state values need their own persistence logic.