History-Item-as-Checkpointer: Context-Lossless Design
Scope: Designing a robust state management system where the dialogue history itself serves as a fine-grained checkpointing mechanism for error recovery and state restoration.
Synthesized from: republic (Tape mechanism), kimi-cli (Checkpoint/Revert), pydantic-ai (Graph State), Codex (Stream Interruption)
1. The Core Problem: Context Fragmentation
Traditional LLM frameworks often treat history as a simple list of messages. When an error occurs (e.g., a network timeout during a long tool call or a stream interruption), the system often faces a binary choice:
- Retry from scratch: Wasteful of tokens and time; loses progress of the current turn.
- Fail completely: Frustrating for users; requires manual recovery.
The "Naive Checkpointer" Goal: Every single item added to the history should automatically provide enough information to resume or rollback the system state to that exact point in time.
2. Design Pattern: The Immutable Tape
The most effective way to implement "each item as a checkpointer" is the Immutable Tape Pattern (inspired by republic).
2.1 The Tape Entry Structure
Instead of just role and content, each history entry should be a "Tape Entry" that captures the system's pulse.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TapeEntry {
/// Monotonically increasing ID (The Checkpoint Index)
pub id: u64,
/// Type of entry
pub kind: EntryKind,
/// The actual data (Message, ToolCall, etc.)
pub payload: serde_json::Value,
/// System state at this moment (e.g., active tool, working directory)
pub state_snapshot: Option<StateDelta>,
/// Metadata (tokens, latency, request_id)
pub meta: HashMap<String, String>,
}
pub enum EntryKind {
System, // System prompt change
User, // User input
Assistant, // Finished assistant message
Streaming, // PARTIAL assistant message (The "Resume Point")
ToolCall, // Request to execute a tool
ToolResult, // Output of a tool
Anchor, // Explicit checkpoint marker
Error, // Captured failure
}
2.2 Implicit vs. Explicit Checkpoints
- Implicit: Every
AssistantorToolResultentry is a natural checkpoint. If the next step fails, we can always revert toid - 1. - Explicit (Anchors): Developers can insert
Anchorentries (e.g., after a successful multi-step task) to mark "milestones" that are safe to return to after major failures.
3. Recovery Mechanisms
3.1 Stream Interruption Recovery (Codex Pattern)
When a stream disconnects halfway:
- Capture Partial Content: Save the received chunks into a
Streamingentry. - Resume Strategy:
- Naive: Delete the
Streamingentry and retry the entire turn. - Advanced: Use the
Streamingentry as a prefix (if the model supports "Prefilling" or "Suffix" completion) to continue where it left off.
- Naive: Delete the
3.2 Tool Call Resumption
If a tool execution fails or the system crashes during execution:
- The history contains the
ToolCallentry but noToolResult. - On recovery, the system scans the tape:
- Found
ToolCallwithout matchingToolResult? → Re-execute or ask user. - This turns history into a "Write-Ahead Log" (WAL) for Agent actions.
- Found
4. Context Filtering (The "View" Pattern)
If history is append-only and grows with every retry/error, how do we prevent prompt pollution?
Recommendation: Separate the Physical Tape from the Logical Context Window.
pub struct ContextBuilder {
pub tape: Vec<TapeEntry>,
}
impl ContextBuilder {
/// Select which entries to actually send to the LLM
pub fn build_prompt(&self, policy: SelectionPolicy) -> Vec<Message> {
match policy {
SelectionPolicy::LastTurnOnly => {
// Find last Anchor or User message and take everything after
}
SelectionPolicy::ExcludeErrors => {
// Filter out 'Error' kinds to keep the prompt clean
}
SelectionPolicy::DeDuplicateTools => {
// If a file was read 5 times, only include the last result
}
}
}
}