← Builder Path / Chapter 10
Architecture position

Capability Extensions

Extend what an agent can do through capability packages, external evidence, persistent state, and standard protocols.

How do we add Skills, retrieval, memory, and external protocol capabilities to an agent?

Concept calibration

Memory

What it is
A mechanism for storing, retrieving, and using state across steps or sessions, with explicit write, read, and forgetting policies.
What it is not
It is not merely compressed chat history, and it should not put every old item back into context.

Context

What it is
The information visible to a model call, including instructions, conversation, tool schemas and results, retrieved evidence, and current state.
What it is not
It is not merely chat history, nor everything the model can remember permanently.

Observability

What it is
Use structured logs, traces, metrics, and version data to explain what happened during a system run.
What it is not
It is not just terminal output; logs without correlation IDs, state transitions, and privacy boundaries are hard to diagnose.

State, Sessions, Memory & Context Compression

Design session state, checkpoints, short- and long-term memory, compression, and replay.

One thing to completeMake long tasks pausable, resumable, compressible, and replayable.
Before you begin, Forge already hasThe accepted solution from Module 09, RAG, Retrieval & Knowledge Evaluation, is this chapter’s starting point.
After this chapter, Forge canPause, resume, and replay long tasks while controlling context growth.
Smallest recovery pointPersist explicit state and the latest checkpoint only; leave long-term memory off.
Download this chapter labPython 3.12 · pytest · Pydantic · SQLite · deterministic Mock
Stage capability chain
01Distinguish current state from historical facts and deterministically rebuild RunState from immutable events02Distinguish a session from an individual run and explicitly manage start, finish, and concurrency boundaries03Persist a minimal versioned checkpoint with integrity verification and reject corrupt or unsupported snapshots04Recover from a checkpoint plus tail events and prove that the result matches a full event replay05Separate recoverable runtime state from retrievable long-term memory with scope, expiry, and deletion semantics06Compress ordinary conversation while preserving instructions, permission decisions, and commitments verbatim with fidelity checks07Replay a run from an auditable event journal and propagate memory deletion to dependent derived-summary caches
Each step adds one verifiable capability; every later step begins from code and tests accepted in the previous step.

Concept calibration\n\nPut the concepts used in this module back inside their engineering boundaries before you work with the code.\n\n
\n\n### Memory memory\n\n- What it is: A mechanism for storing, retrieving, and using state across steps or sessions, with explicit write, read, conflict, retention, and forgetting policies.\n- What it is not: It is not merely a history summary, and it should not put every old record into each context; stored data becomes useful memory only when retrieved and applied correctly.\n- Relationship to adjacent concepts: State represents current progress, checkpoints preserve consistent snapshots, memory reuses information across boundaries, and context carries only the selected portion.\n- Where it lives in HeatStack Forge: Forge stores task state, user approvals, and longer-lived preferences separately, retrieves by version and scope, and supports pause, resume, and replay.\n- Typical misuse and correction: A common misuse writes every model output automatically. Set write thresholds, provenance, expiry, and conflict policy, then test that stale data cannot override current facts.\n\n### Context context\n\n- What it is: The information visible to one model call, including instructions, conversation excerpts, tool schemas and results, retrieved evidence, current state, and output constraints.\n- What it is not: It is not merely chat history or every item in a database; only information selected for the current call belongs to its context.\n- Relationship to adjacent concepts: RAG selects external evidence, memory preserves cross-step state, and tool schemas describe actions. All can supply context, but their responsibilities differ.\n- Where it lives in HeatStack Forge: Forge assembles context by task, permission, budget, and source priority while recording provenance and version for each item.\n- Typical misuse and correction: A common misuse equates more tokens with better understanding. Use selection, truncation, citation, and sensitive-data filters, then verify them on fixed fixtures.\n\n### Observability observability\n\n- What it is: Structured logs, traces, metrics, state transitions, and version information that explain a run and support failure diagnosis and version comparison.\n- What it is not: It is not merely terminal output or unlimited input recording; logs without correlation IDs, state transitions, or privacy boundaries are hard to diagnose and may leak data.\n- Relationship to adjacent concepts: The harness emits runtime events, the agent loop and tool calls form traces, evaluation aggregates metrics, and memory and recovery depend on versioned records.\n- Where it lives in HeatStack Forge: Forge creates an operation_id for each task and records stage, tool, latency, cost, error class, state change, and evidence path while filtering sensitive data.\n- Typical misuse and correction: A common misuse adds logs only after failure. Define events and correlation fields with the contract, then use fault injection to prove the timeline can be reconstructed.\n\n

STAGE 01

Distinguish current state from historical facts and deterministically rebuild RunState from immutable events

Start with a concrete problem

How to rebuild current state from an event sequence?

You are building a long-running task execution system that must record key facts during a run, such as run creation, step start, and permission revocation. These facts are immutable once they occur, but the current state (e.g., whether the run is still active, what the current step is, whether writing is allowed) changes as events are applied.

If you directly mutate a state object, you cannot audit how the state evolved or recover from a crash. Therefore, you need to treat events as an immutable history and rebuild state deterministically by replaying them.

In this stage, you will implement the core of event sourcing: define event and state models, and write an apply_event function that updates state for each event. You must also ensure the event sequence is contiguous and that no events are accepted after the run reaches a terminal state.

Make a prediction

Before implementing reduce_events, predict: if the event sequence has a gap (e.g., sequence 1 followed directly by sequence 3), how should the system handle it?

  • Ignore the gap and continue applying subsequent events
  • Raise an exception because the state cannot be reliably rebuilt
  • Automatically fill in the missing events
  • Log a warning but continue execution

Reasoning guide: The correct answer is to raise an exception. The event sequence must be contiguous because each event depends on the state produced by the previous event. A gap indicates incomplete history, so the system must refuse to rebuild rather than guess or ignore.

What it is

State is a projection of current progress, events are historical facts

State (RunState) represents the latest information about a run at a given moment, such as run status, current step, and write permission. It is derived from the event sequence, not stored independently.

Events (RunEvent) are immutable historical records, each containing a sequence number, type, and payload. Once created, events cannot be modified, so they can be safely replayed.

The apply_event function applies events one by one to the state, producing the current state. This process is deterministic: given the same event sequence, the same state is always produced.

What it is not

State is not history, and events are not mutable objects

State is not a list of all events; it contains only the information needed for the current moment. Historical events may be kept for audit, but state is a condensed projection.

Events are not mutable; once created, they cannot be changed. If an error is discovered, you cannot modify an old event; instead, you append a new event to correct it.

State cannot be read directly from history; it must be rebuilt by replaying events to ensure consistency and auditability.

How it relates to neighboring concepts

Events drive state transitions

Each event corresponds to a state transition. For example, RUN_CREATED creates the initial state, STEP_STARTED updates the current step, and WRITE_PERMISSION_REVOKED sets write_allowed to False.

The event sequence must be contiguous because each event builds on the state produced by the previous event. A gap means the intermediate state is unknown, so the sequence must be rejected.

After the run reaches a terminal state (COMPLETED or FAILED), no new events can be accepted because the state is final; applying further events would cause inconsistency.

Boundary decision for this stage: When the event sequence is not contiguous or the run is terminal, an exception must be raised rather than attempting to repair or ignore the problem. This ensures reliable and auditable state reconstruction.

STAGE 02

Distinguish a session from an individual run and explicitly manage start, finish, and concurrency boundaries

Start with a concrete problem

Why do we need to distinguish sessions from runs?

In the previous stage, we built state and event models, but we still lack the ability to manage multiple runs. Now, a single user session may contain several runs, such as a retry after a failure or parallel processing of multiple tasks. If every run uses the same run_id, histories become mixed, and we cannot tell which run produced which events.

This stage requires implementing a Session container that can start multiple runs, finish runs, and guarantee that run_id is unique within a session. The test test_session_tracks_multiple_runs_without_merging_them verifies that the session correctly tracks multiple runs without merging them; the test test_duplicate_run_id_is_rejected verifies that a duplicate run_id is rejected.

Make a prediction

When implementing start_run, what should happen if an already existing run_id is passed?

  • Silently ignore and do not add a new run
  • Raise ValueError because run_id must be unique
  • Overwrite the old run record
  • Automatically generate a new run_id

Reasoning guide: The correct answer is to raise ValueError. The test test_duplicate_run_id_is_rejected explicitly requires that a duplicate run_id be rejected, and the error message must contain ‘unique’. Silently ignoring or overwriting would cause history loss or confusion, and automatically generating a new ID would violate the caller’s intent.

What it is

A session is a container for runs

Session is an immutable data structure containing a session_id and a tuple of runs. Each run is a RunRecord with a run_id and a status. The start_run function adds a new RunRecord to the session and returns a new Session instance. The finish_run function changes the status of a specified run to a terminal state (completed, failed, or cancelled).

The session lifecycle is explicitly managed by these functions: create session, start run, finish run. Run IDs must be unique within a session, which is guaranteed by a check in start_run.

What it is not

A session is not a single run

A session is not one concrete execution but a container for multiple executions. A run is a single execution with explicit state transitions. Confusing the two leads to state management chaos, such as being unable to distinguish events from different runs.

A session is also not equivalent to chat history or context; it only stores structural information about runs, not the actual event content.

How it relates to neighboring concepts

Relationship among session, runs, and state

Session contains multiple RunRecords, each with a run_id and status. start_run and finish_run are functions that operate on Session and return new Session instances, preserving immutability. The active_run_ids property filters runs with status ‘active’ and returns their IDs.

This design allows session state to be safely passed and shared without accidental modification.

Boundary decision for this stage: In start_run, we must check whether the run_id already exists; if it does, raise ValueError. This is the boundary for run ID uniqueness within a session.

STAGE 03

Persist a minimal versioned checkpoint with integrity verification and reject corrupt or unsupported snapshots

Start with a concrete problem

Saved checkpoints may be tampered with or version-incompatible

During long-running tasks, we need to save the current state to pause, resume, or replay. If a checkpoint file is accidentally modified (e.g., disk error or deliberate tampering), recovery may load an incorrect state, causing subsequent decisions to be based on untrusted data.

Additionally, as the system evolves, the checkpoint structure may change. Loading an old or unknown version may crash due to missing fields or type mismatches, or silently produce wrong results.

This stage requires implementing a minimal checkpoint module with a version number and SHA-256 checksum, verifying both on load and rejecting corrupt or unsupported snapshots.

Make a prediction

When implementing checkpoint loading, which approach most reliably prevents loading tampered state?

  • Only check that the JSON file can be parsed into a CheckpointEnvelope model.
  • Recompute the SHA-256 checksum of the state on load and compare it with the stored checksum.
  • Trust file system permissions, assuming only authorized users can modify the file.
  • Only check that schema_version equals the current version on load.

Reasoning guide: The correct option is to recompute the checksum and compare. Parsing JSON or checking the version alone cannot detect content tampering; file system permissions cannot prevent all accidental modifications; version checking only handles structural compatibility, not data integrity.

What it is

Checkpoint Contract

A checkpoint is an immutable envelope (CheckpointEnvelope) containing schema_version, state, and checksum_sha256. schema_version identifies the structure version, state is the current run state, and checksum_sha256 is the SHA-256 hash of the canonical JSON representation of schema_version and state.

On save, we compute the checksum and atomically write the file; on load, we first verify that schema_version is supported, then recompute the checksum and compare it with the stored value, raising ValueError on any mismatch.

What it is not

What a Checkpoint Is Not

A checkpoint is not a simple serialization of state; it must include an integrity proof. It is also not a substitute for long-term memory or session history; it only saves a snapshot of state at a point in time.

A checkpoint does not decide when to save or restore, nor does it handle concurrent write conflicts; those policies are determined by upper-level session management.

How it relates to neighboring concepts

Relationships with Other Concepts

A checkpoint is closely related to state (RunState): it encapsulates the state and provides persistence. Unlike context, a checkpoint does not automatically enter the model context; only after explicit loading and selection of part of the state does it become part of the context.

In terms of observability, the checkpoint’s version and checksum provide an audit trail, helping diagnose recovery failures.

Boundary decision for this stage: When loading a checkpoint, both schema_version and checksum_sha256 must be verified; any failure should reject recovery rather than attempt partial loading or silent repair.

STAGE 04

Recover from a checkpoint plus tail events and prove that the result matches a full event replay

Start with a concrete problem

How can we prove that state recovered from a checkpoint matches a full event replay exactly?

During long-running tasks, the system periodically saves checkpoints to support pause and resume. When a task is interrupted, we want to restore the run state by loading a checkpoint and replaying the events that occurred after it (the tail events).

However, simply loading a checkpoint and applying tail events does not guarantee correct recovery. If the tail events are incomplete (for example, one event is missing), the recovered state may differ from the state obtained by replaying all events from the beginning.

In this stage, you will implement a verification function, verify_recovery_equivalent, that compares the state recovered from checkpoint plus tail events with the state from a full event replay, and raises RecoveryMismatch if they differ.

Make a prediction

Before implementing recovery equivalence verification, predict: what happens if a tail event is omitted during recovery from a checkpoint?

  • The recovered state may differ from full replay, but the system cannot automatically detect it
  • The recovered state will always match full replay because the checkpoint contains all necessary information
  • The system will raise an exception because the checkpoint checksum detects the missing event
  • The recovered state may differ, but it can be manually checked via logs

Reasoning guide: The correct answer is the first option. A checkpoint only stores a snapshot of state at a certain point; it cannot know which events should occur afterwards. If tail events are incomplete, the recovered state will miss the effects of those events, causing a mismatch with full replay. Therefore, an explicit comparison is needed to detect this inconsistency.

What it is

What recovery equivalence verification is

Recovery equivalence verification is a safety mechanism that compares the results of two computation paths to ensure correct recovery: path one loads state from a checkpoint and applies tail events; path two replays the full event history from the beginning.

If the states from both paths are exactly equal, it proves that the checkpoint and tail events are sufficient to reconstruct the full state; if they differ, it indicates missing or out-of-order tail events, and an exception must be raised.

What it is not

What recovery equivalence verification is not

It is not checkpoint checksum verification. A checksum only ensures the checkpoint file has not been tampered with; it cannot guarantee tail event completeness.

It is not a superficial state comparison; it must actually perform a full replay because state may contain complex objects and surface-level field checks cannot determine deep consistency.

How it relates to neighboring concepts

Relationship with other concepts

A checkpoint saves a state snapshot at a certain moment and serves as the starting point for recovery.

Event replay builds state by applying events in sequence and represents the complete path.

Recovery equivalence verification combines both: the checkpoint provides acceleration, tail events provide increments, and full replay provides the baseline.

Boundary decision for this stage: Recovery is considered equivalent if and only if the state recovered from checkpoint plus tail events is exactly equal to the state from full event replay; otherwise, a RecoveryMismatch exception must be raised to prevent incorrect state from propagating.

STAGE 05

Separate recoverable runtime state from retrievable long-term memory with scope, expiry, and deletion semantics

Start with a concrete problem

Separate long-term memory from runtime state and ensure memory is retrievable only within correct scope and validity period

In the previous stage, we implemented recoverable runtime state but no long-term memory. Now we need to add forge/memory.py, define MemoryScope (RUN, SESSION, USER) and MemoryRecord, and implement MemoryStore methods put, query, delete, and purge_expired.

Key constraints: queries must filter by scope and subject and exclude expired memories; deletion must be explicit and idempotent. Tests test_memory_is_queried_only_inside_its_scope_and_subject, test_expired_memory_is_hidden_and_can_be_purged, and test_deletion_is_explicit_and_idempotent will verify these behaviors.

The current starting code has no forge/memory.py, so running the tests will raise ModuleNotFoundError. You need to create that file from scratch and modify forge/init.py to export the relevant symbols.

Make a prediction

When implementing MemoryStore.query, what happens if you filter only by scope and subject_id without checking expires_at?

  • Expired memories will still be returned, causing later decisions to use stale information.
  • The query will raise an exception because the SQL statement is incomplete.
  • Expired memories will be automatically deleted by the database, so they won’t be returned.
  • The query results will be sorted by creation time but will not include expired memories.

Reasoning guide: The correct option is the first one. If the query does not check expires_at, then even if a memory is expired, as long as scope and subject_id match, it will be returned. This causes expired memories to contaminate the current context and affect decisions. The test test_expired_memory_is_hidden_and_can_be_purged specifically verifies this.

What it is

Memory boundary model

Memory is a data record with a scope and a lifetime. The scope (RUN, SESSION, USER) defines the visibility range of the memory: RUN is visible only within a single run, SESSION within a session, and USER across sessions long-term.

Each memory has created_at and optional expires_at. When querying, the system filters out expired memories based on the current time. Deletion is performed by memory_id precisely, and repeated deletion does not raise an error; it returns False to indicate no record was deleted.

What it is not

What memory boundaries are not

Memory is not a simple compressed chat transcript, nor does it put all historical information into context. It must define write, read, conflict, retention, and forgetting policies.

Memory is not the same as runtime state. Runtime state (such as current step, retry count) is volatile, while memory is persistent and can be reused across runs.

How it relates to neighboring concepts

Relationship between memory and other concepts

Memory and context: Context is the information visible to one model call, and memory is one possible source of context. Only memories that are queried and placed into context affect the current call.

Memory and state: State represents current progress, while memory preserves information across steps or sessions. State can be restored via checkpoints, while memory manages its lifecycle through scope and expiry policies.

Boundary decision for this stage: When implementing MemoryStore.query, you must filter by scope, subject_id, and expires_at simultaneously to ensure only currently valid memories within the requested scope are returned.

STAGE 06

Compress ordinary conversation while preserving instructions, permission decisions, and commitments verbatim with fidelity checks

Start with a concrete problem

How can we compress context without losing critical information?

In long tasks, the context window is limited, so ordinary messages must be compressed to save space. However, compression must not affect critical information: instructions, permission decisions, and commitments must be preserved verbatim, otherwise the system may perform unauthorized actions.

Currently, forge/context.py has not been created, and tests fail on import. You need to implement ContextPacket, CompressedContext, compress_context, and verify_critical_fidelity, ensuring that the compressed critical fields are exactly identical to the source, and any modification triggers a fidelity check failure.

Make a prediction

When implementing verify_critical_fidelity, what should happen if the compressed permission_decisions differ from the source?

  • Silently accept, because compression may change permissions
  • Raise ValueError, because permission decisions must be preserved verbatim
  • Log a warning but continue
  • Automatically restore the source permissions

Reasoning guide: The correct option is to raise ValueError. Permission decisions are critical context; any change indicates a security risk and must fail immediately. Silently accepting or auto-restoring could mask tampering.

What it is

Context compression fidelity model

Context compression summarizes ordinary messages into a short text while copying critical fields (instructions, permission decisions, commitments) verbatim into the compressed result.

Critical fields are bound by a SHA-256 fingerprint. Verification compares each field and checks the fingerprint to ensure no critical information was lost or tampered with during compression.

What it is not

Not indiscriminate summarization

Compression is not summarization of all content. Critical fields cannot be summarized because summarization may change semantics, e.g., turning “write_access=revoked” into “write access discussed”.

Compression is also not simple truncation. Ordinary messages may be truncated, but critical fields must remain complete, otherwise subsequent steps cannot rely on this information.

How it relates to neighboring concepts

Component relationships

ContextPacket is the source data containing all fields. compress_context generates CompressedContext, where critical fields are copied directly and ordinary messages are summarized.

verify_critical_fidelity receives the source and compressed result, compares critical fields and verifies the fingerprint. If any critical field mismatches or the fingerprint is inconsistent, it raises ValueError.

Boundary decision for this stage: Critical fields (instructions, permission decisions, commitments) must be preserved verbatim and fingerprinted; ordinary messages may be compressed. During verification, any difference in critical fields or fingerprint mismatch must cause failure.

STAGE 07

Replay a run from an auditable event journal and propagate memory deletion to dependent derived-summary caches

Start with a concrete problem

After deleting memory, derived summary caches still expose deleted content

In the previous stage, we implemented context compression, but the system still lacks an auditable event journal and a safe memory deletion mechanism. Currently, the file forge/replay.py does not exist, so tests fail at import time (ModuleNotFoundError: No module named ‘forge.replay’).

This stage requires implementing two core mechanisms: first, a hash-chained event journal that ensures the log is auditable and tamper-evident; second, when deleting memory, explicitly propagate the deletion to all derived summary caches that depend on that memory, preventing caches from continuing to expose deleted sensitive information.

Specifically, the test test_deletion_propagates_to_derived_summary_cache creates a user memory ‘private-note’ and generates two derived summaries: summary-1 references private-note, summary-2 does not. After calling delete_memory_everywhere, it expects summary-1 to be invalidated (invalidated == 1) while summary-2 remains unchanged.

The current failure fixture (fixtures/failure.json) shows that delete_memory_everywhere returns immediately after deleting the memory from the primary store without calling DerivedSummaryCache.invalidate_for_memory, causing the test to fail.

Make a prediction

When implementing delete_memory_everywhere, what happens if you only delete the memory from the primary store and do not handle the derived summary cache?

  • The derived summary cache updates automatically; no extra action is needed.
  • The derived summary cache still retains the deleted memory’s content, leading to data leakage.
  • The system throws an exception because the cache is inconsistent with the primary store.
  • The deletion fails because there are references in the cache.

Reasoning guide: The correct answer is the second option. The derived summary cache is stored independently and does not automatically detect changes in the primary store. Without explicit invalidation, the cache continues to expose the deleted memory content, violating data deletion integrity.

What it is

Relationship between event journal and derived summary cache

The event journal (EventJournal) is an append-only, hash-chained audit record. Each record contains the hash of the previous record (previous_hash) and the hash of the current event (record_hash). Any tampering causes a hash mismatch and is detected.

The derived summary cache (DerivedSummaryCache) stores summaries generated from multiple memories and records the list of source memory IDs (source_memory_ids) for each summary. When deleting a memory, you must find and delete all summaries that reference that memory; otherwise, the cache retains stale content.

The delete_memory_everywhere function coordinates the deletion: first delete the memory from the primary store, then call summary_cache.invalidate_for_memory(memory_id) to invalidate all related summaries and return the invalidation count.

What it is not

Common misconceptions

The event journal is not a plain text file; it uses a hash chain to guarantee integrity, and any modification breaks the chain.

The derived summary cache does not automatically synchronize with the primary store; explicit invalidation is required.

Deleting memory is not a single-point operation; it must propagate to all dependent derived data.

How it relates to neighboring concepts

Component collaboration

EventJournal uses RunEvent and reduce_events to replay state, ensuring that the state recovered from the journal matches the original state.

DerivedSummaryCache establishes dependencies between summaries and memories via the source_memory_ids field; the invalidate_for_memory method uses this field to find and delete related summaries.

delete_memory_everywhere connects MemoryStore and DerivedSummaryCache to implement cross-store deletion propagation.

Boundary decision for this stage: When deleting memory, you must handle both the primary store and all derived caches; deleting only from the primary store while ignoring caches leads to data inconsistency and potential leakage.

Complete the chapter

Pause, resume, and replay long tasks while controlling context growth.

FORGE / LOCAL CHECK

Local lab self-check

  1. Not started
  2. 2Reading
  3. 3Lab downloaded
  4. 4Test result read
  5. 5Local check passed

verification.json is parsed only in this browser and is never uploaded. This checks the lab contract; it is not server certification or third-party endorsement.

Concept calibration and one-week review

Three questions check the chapter's boundaries, delivery evidence, and recovery path. Answers stay in this browser.

  1. 1Which result best proves that you completed “State, Sessions, Memory & Context Compression”?
  2. 2Which statement most accurately describes “Memory”?
  3. 3When the lab is blocked, which action is the chapter's recommended minimum recovery point?